
백엔드 개발자를 준비하며 생기는 의문들을 정리한 포스트입니다.
TDD란?Test-Driven Development의 약자로 소프트웨어 개발에서 테스트를 먼저 작성하고
그 테스트를 통과하는 코드를 개발하는 방식을 말합니다.
미리 테스트를 적용하는 이유는 미리 코드를 작성해서 서비스를 도입하면 수정할때마다
서버를 내렸다 다시올려야합니다. 이는 곧 서버의 필요없는 부하가 이루어집니다.
이를 미리 방지하고자 테스트코드를 미리 작성하는 습관을 들이는 것이 바람직합니다.
테스트 코드는 작성한 코드가 의도대로 잘 동작하고 예상치 못한 문제가 없는지 확인할 목적으로
작성하는 코드입니다.
스프링 부트의 테스트는 루트 디렉터리 > src > test 디렉터리에서 작업합니다.
테스트 코드의 패턴은 다양하며 그 중 가장 널리 사용되는 단위 테스트 중심 패턴 중
Given-When-Then(GWT)를 알아보려고 합니다.
GWT 의 의미Given : 테스트 전체 조건 설정
When : 실행 동작 정의
Then : 결과 검증

실패하는 테스트를 작성하여 요구사항에 명확한 목표를 설정합니다.
테스트 통과에 꼭 필요한 최소한의 코드만 작성합니다.
테스트 통과한 최소 코드를 개선합니다. (중복제거, 가독성 향상을 목표 -> 유지보수성 증가)
스프링 부트는 애플리케이션을 테스트 위한 도구와 어노테이션을 제공합니다.
testImplementation("org.springframework.boot:spring-boot-starter-test")
JUnit은 자바 언어를 위한 단위 테스트 프레임워크이지만 코틀린은 자바에 100% 호환합니다.
해당 의존성은 단위 테스트를 위한 프레임워크입니다.
단위테스트란 작성한 코드가 의도대로 작동되는지 작은 단위로 검증하는 것을 의미하며
JUnit이 사용법이 간단하고 결과가 직관적이기 때문에 많이 사용됩니다.
testImplementation("io.kotest:kotest-runner-junit5:5.9.0") // Kotest optional
Java의
Mockito테스트할 때 의존성 객체의 동작을 가짜(mock)로 만들어주는 라이브러리를
Kotlin 친화적으로 만든 라이브러리입니다.
final 클래스와 메서드를 기본적으로 지원하기 때문에
람다식이나 확장 함수를 편안하게 사용할 수 있습니다.
testImplementation("io.mockk:mockk:1.13.5")
저는 사람들의 데이터를 저장하기 위한 Human 서비스를 출시할 예정입니다.
사람들의 이름과 나이를 저장받고 이를 불러오는 서비스를 개발하였습니다.
하지만 서비스를 만들고 보니 이 서비스가 제대로 동작하는지 검증하고 싶어졌습니다.
그래서 테스트코드를 작성하려고 합니다.
//controller
@RestController
@RequestMapping("/humans")
class HumanController (
private val humanService: HumanService
){
@GetMapping
private fun getHumans() : ResponseEntity<List<HumanResponseDto>>{
val result = humanService.getHuman()
return ResponseEntity.status(HttpStatus.OK).body(result)
}
@PostMapping
private fun createHuman(@RequestBody humanRequestDto: HumanRequestDto) : ResponseEntity<HumanResponseDto>{
val result = humanService.createHuman(humanRequestDto)
return ResponseEntity.status(HttpStatus.CREATED).body(result)
}
}
//dto
data class HumanRequestDto (
val id : Long,
val name : String,
val age : Int
){
fun toEntity() : Human = Human(
id = id,
name = name,
age = age
)
}
data class HumanResponseDto (
val name : String,
val age : Int
)
//entity
@Entity
data class Human(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id : Long = 0,
@Column(nullable = true)
val name : String,
@Column(nullable = true)
val age : Int,
) {
fun toResponse() : HumanResponseDto = HumanResponseDto (
name = name,
age = age
)
}
//repository
interface HumanRepository : JpaRepository<Human, Long>
//service
@Transactional
@Service
class HumanService (
private val humanRepository: HumanRepository
){
fun getHuman() : List<HumanResponseDto>{
val result = humanRepository.findAll()
return result.map { it.toResponse() }
}
fun createHuman(humanRequestDto: HumanRequestDto) : HumanResponseDto {
val result = humanRepository.save(humanRequestDto.toEntity())
return result.toResponse()
}
}
위의 서비스가 있을 때 서비스들이 제대로 동작하기 위한 테스트 코드
@SpringBootTest
class humanServiceTest {
private val humanRepository: HumanRepository = mockk()
private val humanService = HumanService(humanRepository)
@Test
fun`사람 생성 테스트`() {
//given
val request = HumanRequestDto(id = 1L, name = "DH", age = 24)
val saveHuman = Human(id = 1L, name = "DH", age = 24)
every { humanRepository.save(any()) } returns saveHuman
//when
val result = humanService.createHuman(request)
//then
assertEquals("DH", result.name)
assertEquals(24, result.age)
verify(exactly = 1) { humanRepository.save(any()) }
}
@Test
fun `사람 전체 조회 테스트`() {
// given
val humans = listOf(
Human(id = 1L, name = "짱구", age = 5),
Human(id = 2L, name = "철수", age = 5),
Human(id = 3L, name = "맹구", age = 5),
Human(id = 4L, name = "유리", age = 5),
Human(id = 5L, name = "훈이", age = 5),
)
every { humanRepository.findAll() } returns humans
// when
val result = humanService.getHuman()
// then
assertEquals(5, result.size)
assertEquals("짱구", result[0].name)
assertEquals("철수", result[1].name)
assertEquals("맹구", result[2].name)
assertEquals("유리", result[3].name)
assertEquals("훈이", result[4].name)
verify(exactly = 1) { humanRepository.findAll() }
}
}
GWT를 따라
Given에서는 테스트 전체 조건 설정을 해주기 위해
Human 서비스의 요청이 어떤것이 들어오는지 정의를 해줍니다.
다음When에서는 실행 동작을 변수 하나에 저장을 해주고
Then에서는 결과 검증assertEquals()을 통해 데이터베이스상에 제대로 저장되었을까와
verify를 통해 해당 서비스가 한번만 호출되었는지 검증해줍니다.
이를 통해 서비스를 직접 실행하지 않고도 테스트코드만 작성하면
서비스에 대한 유지보수를 쉽게 할 수 있습니다.
| 함수 | 설명 |
|---|---|
assertEquals(expected, actual) | 두 값이 같은지 비교 |
assertNotEquals(expected, actual) | 두 값이 다른지 비교 |
assertTrue(condition) | 조건이 true인지 확인 |
assertFalse(condition) | 조건이 false인지 확인 |
assertNull(value) | 값이 null인지 확인 |
assertNotNull(value) | 값이 null이 아닌지 확인 |
assertIterableEquals(expected, actual) | 두 리스트/컬렉션이 같은지 비교 |
assertThrows<예외타입> { } | 특정 예외가 발생하는지 검증 |
| 함수 | 설명 |
|---|---|
verify { 함수호출() } | 해당 함수가 호출됐는지 확인 |
verify(exactly = n) { 함수호출() } | 정확히 n번 호출됐는지 검증 |
verify(atLeast = n) { 함수호출() } | 최소 n번 호출됐는지 검증 |
verify(atMost = n) { 함수호출() } | 최대 n번 호출됐는지 검증 |
verifyOrder { 호출1(); 호출2() } | 호출 순서대로 실행됐는지 검증 |
verifyAll { 호출1(); 호출2() } | 나열한 모든 호출이 일어났는지 검증 |
confirmVerified(mock) | 더 이상 호출이 없어야 통과 |
TDD는 미리 서비스를 출시하기 전 내가 작성한 코드대로 동작하는지 확인하고 개발하는 방법입니다.
그래서 서버를 다운시키지 않고도 확인할 수 있어 코드 유지보수에 용의하다는 매우 큰 장점이 있으니
테스트코드를 잘 활용하여 신뢰할 수 있는 코드를 만들어봅시다.
[Spring Boot] TDD와 given-when-then 패턴으로 테스트 코드 작성 (JUnit, AssertJ, 단위 테스트, 통합 테스트, Mock 등)