본문 바로가기
서버/Kotlin-Spring_Boot

Kotlin-Spring_Boot 강의 정리) 7. GET Single Bank

by HDobby 2023. 1. 30.

https://youtu.be/jiv37i503v4

BASE_URL/banks/1234와 같이 1234에 해당하는 bank를 가져오는 api를 만들어보겠습니다.

 

@Test
	fun `should return the bank with the given account number`() {
		// given
		val accountNumber = 1234

		// when
		mockMvc.get("/api/banks/$accountNumber")

			// then
			.andDo { print() }
			.andExpect { status { isOk() } }

	}

전에 만들었었던 BankControllerTest에 해당 함수를 추가해 줍시다.

 

실행을 해보면? 익숙하게도 실패하게 됩니다. /api/banks/$accountNumber에 관한 endpoint가 설정되어 있지 않아서 그렇습니다.

 

@GetMapping("/{accountNumber}")
fun getBBank(@PathVariable accountNumber: String) = service.getBank(accountNumber)

가 최종적으로 완성될 내용이지만, 아직 getBank가 없으므로 

 

@GetMapping("/{accountNumber}")
fun getBBank(@PathVariable accountNumber: String) = "You want data about $accountNumber"

를 BankController로 가서 추가해 줍시다.

 

기존에 만들어 놨었던 MockBankDataSource를 보면

@Repository
class MockBankDataSource : BankDataSource {
   val banks = listOf(
      Bank("1234", 3.14, 18),
      Bank("110", 17.0, 0),
      Bank("5678", 0.0, 100),
   )

   override fun retrieveBanks(): Collection<Bank> = banks
}

1234, 3.14, 18로 구성되어 있는 걸 볼 수 있습니다.

테스트에서 이 데이터를 체크해 주도록 합시다.

@Test
	fun `should return the bank with the given account number`() {
		// given
		val accountNumber = 1234

		// when
		mockMvc.get("/api/banks/$accountNumber")

			// then
			.andDo { print() }
			.andExpect {
				status { isOk() }
				content { MediaType.APPLICATION_JSON }
				jsonPath("$.trust") { value(3.14) }
				jsonPath("$.transactionFee") { value(18) }
			}
	}

BankController의 내용 또한 원본으로 바꿔주도록 합시다.

@RestController
@RequestMapping("/api/banks")
class BankController(private val service: BankService) {

	@GetMapping
	fun helloWorld(): Collection<Bank> = service.getBanks()

	@GetMapping("/{accountNumber}")
	fun getBBank(@PathVariable accountNumber: String) = service.getBank(accountNumber)
}

BankService에 getBank를 추가해 줍시다.

@Service
class BankService(private val dataSource: BankDataSource) {
	fun getBanks(): Collection<Bank> {
		return dataSource.retrieveBanks()
	}

	fun getBank(accountNumber: String): Bank = dataSource.retrieveBank(accountNumber)
}

BankDataSource에 retrieveBank를 추가해 주러 갑시다.

interface BankDataSource {

	fun retrieveBanks(): Collection<Bank>

	fun retrieveBank(accountNumber: String): Bank
}

BankDataSource를 implement 했던 MockBankDataSource에 retrieveBank를 추가해 줍니다.

@Repository
class MockBankDataSource : BankDataSource {
	val banks = listOf(
		Bank("1234", 3.14, 18),
		Bank("110", 17.0, 0),
		Bank("5678", 0.0, 100),
	)

	override fun retrieveBanks(): Collection<Bank> = banks

	override fun retrieveBank(accountNumber: String): Bank {
		return banks.first { it.accountNumber == accountNumber }
	}
}
override fun retrieveBank(accountNumber: String): Bank =
		banks.first { it.accountNumber == accountNumber }

와 같은 방식으로 수정해 줄수도 있습니다.

 

nested class에 관한 live template을 추가 해줍시다.

이전에 test를 만들었던 것과 동일한 방법으로 하면됩니다.

 

settings > Editor > Live Templates > Kotlin

@Nested
@DisplayName("")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class {

}

Junit이 모든 테스트 케이스의 실행시마다 동적 객체 생성을 지원하지 않기때문에 우리가 수동으로 만들어주기 위해 사용합니다.

@Nested
	@DisplayName("getBanks()")
	@TestInstance(TestInstance.Lifecycle.PER_CLASS)
	inner class GetBanks {
		@Test
		fun `should return all banks`() {
			// when/then
			mockMvc.get("/api/banks")
				.andDo { print() }


				//then
				.andExpect {
					status { isOk() }
					content { contentType(MediaType.APPLICATION_JSON) }
					jsonPath("$[0].accountNumber") { value("1234") }
				}

		}
	}

getBanks()와 관련된 테스트를 사용할때에는 해당 클래스 내부에 넣어두고 사용하는 식으로 캡슐화가 가능해집니다.

 

이번엔 getBank를 캡슐화 해봅시다.

@Nested
	@DisplayName("getBank()")
	@TestInstance(TestInstance.Lifecycle.PER_CLASS)
	inner class getBank {
		@Test
		fun `should return the bank with the given account number`() {
			// given
			val accountNumber = 1234

			// when
			mockMvc.get("/api/banks/$accountNumber")

				// then
				.andDo { print() }
				.andExpect {
					status { isOk() }
					content { MediaType.APPLICATION_JSON }
					jsonPath("$.trust") { value(3.14) }
					jsonPath("$.transactionFee") { value(18) }
				}
		}
	}

실행을 해보면? 왠일로 정상적으로 통과가 되게 됩니다.

만약 에러가 나는 경우 gradle로 실행을 한게 문제가 되는걸 수 있습니다.

정상적으로 실행이 되었다면

클래스를 실행해 좌측에 해당 창이 나오는지 확인해봅시다. 이런식으로 함수내부에 어떤 테스트가 성공했는지 실패했는지를 직관적으로 보기 편하게 보여줍니다.

 

이번엔 getBank에 없는 accountNumber를 요청하면 정상작동하는지를 테스트 해봅시다.

 

그런데 /api/banks의 주소가 중복되어 계속 사용되므로 변수로 만들어 묶도록 합시다.

val baseUrl = "/api/banks"

fun `should return all banks`() {
			// when/then
			mockMvc.get(baseUrl)
            
fun `should return the bank with the given account number`() {
			// given
			val accountNumber = 1234

			// when
			mockMvc.get("$baseUrl/$accountNumber")
            
fun `should return Not Found if the account number does not exist`() {
			// given
			val accountNumber = "does_not_exist"

			// when/then

			mockMvc.get("$baseUrl/$accountNumber")

후에 should return Not Found 부분을 완성해 봅시다.

@Test
		fun `should return Not Found if the account number does not exist`() {
			// given
			val accountNumber = "does_not_exist"

			// when/then

			mockMvc.get("$baseUrl/$accountNumber")
				.andDo { print() }
				.andExpect { status { isNotFound() } }

		}

실행하면

매칭되는 엘리먼트가 없다고 나오고 실패를 하게 됩니다. 우리가 원하던 결과인데 어째서 실패를 한 걸까요?

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.util.NoSuchElementException: Collection contains no element matching the predicate.

적절한 엘리먼트가 없다고 예외가 발생해 적절한 response대신 request에 에러가 나와 에러가 나오게 된 것 같습니다.

 

BankController로 가서 ExceptionHandler를 만들어 봅시다.

@RestController
@RequestMapping("/api/banks")
class BankController(private val service: BankService) {

	@ExceptionHandler(NoSuchElementException::class)
	fun handleNotFound(e: NoSuchElementException): ResponseEntity<String> =
		ResponseEntity(e.message, HttpStatus.NOT_FOUND)

	@GetMapping
	fun helloWorld(): Collection<Bank> = service.getBanks()

	@GetMapping("/{accountNumber}")
	fun getBBank(@PathVariable accountNumber: String) = service.getBank(accountNumber)
}

해당 예외가 발생한 경우 ResponseEntity를 전송하게 됩니다.

다시 테스트를 실행해보면? 통과하게 됩니다.

하지만 Controller가 많아지거나 각기 다른 에러메시지가 필요할 수 있습니다.

MockBankDataSource 에 retrieveBank를 수정하여 에러처리도 가능합니다..

	override fun retrieveBank(accountNumber: String): Bank =
		banks.firstOrNull() { it.accountNumber == accountNumber }
			?: throw NoSuchElementException("Could not find a bank with account number $accountNumber aaa")

실행하면

우리가 적었던 에러메시지가 나오게 됩니다.

728x90

댓글