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")
실행하면

우리가 적었던 에러메시지가 나오게 됩니다.
'서버 > Kotlin-Spring_Boot' 카테고리의 다른 글
| Kotlin-Spring_Boot 강의 정리) 9. PATCH Endpoint (0) | 2023.02.02 |
|---|---|
| Kotlin-Spring_Boot 강의 정리) 8. POST Endpoint (1) | 2023.01.31 |
| Kotlin-Spring_Boot 강의 정리) 6. Web Layer (0) | 2023.01.30 |
| Kotlin-Spring_Boot 강의 정리) 5. Service Layer (0) | 2023.01.29 |
| Kotlin-Spring_Boot 강의 정리) 4. Data Source (0) | 2023.01.28 |
댓글