오늘은 POST 테스트 방법에 대해 알아봅시다.
시작하기 전에 앞서 이전에 작성해 놨던 DisplayName을 조금 더 구체적으로 변경해 줍시다.
getbanks() -> GET /api/banks
getbank() -> GET /api/bank/{accountNumber}
로 변경해 주도록 합시다.
@Nested
@DisplayName("POST /api/banks")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class {
@Test
fun `should add the new bank`() {
// given
val newBank = Bank("acc123", 31.415, 2)
// when
mockMvc.post(baseUrl) {
contentType = MediaType.APPLICATION_JSON
}
// then
.andDo { print() }
.andExpect {
status { isCreated() }
}
}
}
post를 테스트하기 위해 위와 같이 작성했습니다.
mockMvc.post를 테스트할 때 콘텐츠 타입뿐 아니라, 콘텐츠도 테스트를 해야 하는데 이때 잭슨이라는 라이브러리를 사용할 수 있습니다.
이전에 만들었던 MockVmc와 같이 ObjectMapper를 만들어 봅시다.
@Autowired
lateinit var mockMvc: MockMvc
@Autowired
lateinit var objectMapper: ObjectMapper
val baseUrl = "/api/banks"
와 같이 선언을 여러 번 해줄 수도 있지만 좀 더 간단하게 줄여봅시다.
internal class BankControllerTest @Autowired constructor(
val mockMvc: MockMvc,
val objectMapper: ObjectMapper,
) {
val baseUrl = "/api/banks"
와 같은 방식으로도 줄일 수 있습니다.
@Nested
@DisplayName("POST /api/banks")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class PostNewBank {
@Test
fun `should add the new bank`() {
// given
val newBank = Bank("acc123", 31.415, 2)
// when
val performPost = mockMvc.post(baseUrl) {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(newBank)
}
// then
performPost.andDo { print() }
.andExpect {
status { isCreated() }
}
}
}
테스트 코드를 다음과 같이 작성해 주었습니다.
json은 조금 더 복잡한 구조를 가지고 있으므로 string으로 변환합니다.
when과 then 블록을 명확하게 구분하기 위해 변수로 저장하여 결과를 저장한 뒤 실행해 주었습니다.
BankController로 이동하여 Post함수를 작성해 줍시다.
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)
@PostMapping
fun addBank(@RequestBody bank: Bank): Bank = bank // TODO
}
bank가 중복되어 헷갈릴 수 있지만, 일단 테스트를 실행해 봅시다.
@RequestBody 사용하여 요청으로 들어온 body 그대로를 전달받습니다.

201번이 아닌 200번이 와서 에러가 발생했다고 합니다.
@ResponseStatus(HttpStatus.CREATED)
을 @PostMapping 어노테이션 아래에 추가해 줍시다.
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun addBank(@RequestBody bank: Bank): Bank = bank // TODO
다시 실행해 보면

성공하게 됩니다.
전송받은 데이터가 똑바로 저장이 되었는지 확인하기 위해 then블록에 내용을 조금 추가해 줍시다.
// then
performPost.andDo { print() }
.andExpect {
status { isCreated() }
content { contentType(MediaType.APPLICATION_JSON) }
jsonPath("$.accountNumber") { value("acc123") }
jsonPath("$.trust") { value("31.415") }
jsonPath("$.transactionFee") { value("2") }
}
실행해 보면?

통과하게 됩니다.
BankController로 가서 addBank 함수를 변경해 줍시다.
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun addBank(@RequestBody bank: Bank): Bank = service.addBank(bank)
service에도 addBank 함수를 추가해 줍니다.
@Service
class BankService(private val dataSource: BankDataSource) {
fun getBanks(): Collection<Bank> {
return dataSource.retrieveBanks()
}
fun getBank(accountNumber: String): Bank = dataSource.retrieveBank(accountNumber)
fun addBank(bank: Bank): Bank = dataSource.createBank(bank)
}
BankDataSource로 가서도 추가합시다.
interface BankDataSource {
fun retrieveBanks(): Collection<Bank>
fun retrieveBank(accountNumber: String): Bank
fun createBank(bank: Bank): Bank
}
MockBankDataSource의 banks를 보면 listOf로 되어 있는데 추후 추가변경이 되지 않습니다. 리스트를 mutableListOf로 변경한 뒤 createBank를 만들어 줍시다.
@Repository
class MockBankDataSource : BankDataSource {
val banks = mutableListOf(
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 =
banks.firstOrNull() { it.accountNumber == accountNumber }
?: throw NoSuchElementException("Could not find a bank with account number $accountNumber aaa")
override fun createBank(bank: Bank): Bank {
banks.add(bank)
return bank
}
}
실행해 보면 테스트를 통과하게 됩니다.

이번엔 accountNumber가 이미 존재하는 요청이 들어온 경우를 테스트해 봅시다.
PostNewBank 클래스 내부에 테스트를 추가해 줍니다.
@Test
fun `should return BAD REQUEST if bank with given accountnubmer already exist`() {
// given
val invalidBank = Bank("1234", 12.3, 12)
// when
val performPost = mockMvc.post(baseUrl) {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(invalidBank)
}
// then
performPost
.andDo { print() }
.andExpect { status { isBadRequest() } }
}

400이 아닌 201이 발생하네요. data source를 가서 확인해 봅시다. 중복에 대한 예외처리가 되어있지 않습니다. 추가해 줍시다.
override fun createBank(bank: Bank): Bank {
if (banks.any { it.accountNumber == bank.accountNumber }) {
throw IllegalArgumentException("Bank with account number ${bank.accountNumber} already exist")
}
banks.add(bank)
return bank
}
저번 강의에서 했던 것과 같이 Controller로 가서 예외처리를 해줍시다.
@ExceptionHandler(NoSuchElementException::class)
fun handleNotFound(e: NoSuchElementException): ResponseEntity<String> =
ResponseEntity(e.message, HttpStatus.NOT_FOUND)
@ExceptionHandler(IllegalArgumentException::class)
fun handleNotFound(e: IllegalArgumentException): ResponseEntity<String> =
ResponseEntity(e.message, HttpStatus.BAD_REQUEST)

우리가 작성했던 response가 똑바로 들어온 것을 확인할 수 있습니다.
'서버 > Kotlin-Spring_Boot' 카테고리의 다른 글
| Kotlin-Spring_Boot 강의 정리) 10. DELETE Endpoint (0) | 2023.02.03 |
|---|---|
| Kotlin-Spring_Boot 강의 정리) 9. PATCH Endpoint (0) | 2023.02.02 |
| Kotlin-Spring_Boot 강의 정리) 7. GET Single Bank (0) | 2023.01.30 |
| Kotlin-Spring_Boot 강의 정리) 6. Web Layer (0) | 2023.01.30 |
| Kotlin-Spring_Boot 강의 정리) 5. Service Layer (0) | 2023.01.29 |
댓글