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

Kotlin-Spring_Boot 강의 정리) 8. POST Endpoint

by HDobby 2023. 1. 31.

https://youtu.be/uqQI_BnaHU4

오늘은 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가 똑바로 들어온 것을 확인할 수 있습니다.

728x90

댓글