테스트 주도 개발 (Test-driven development TDD)
여기서 가장 중요한 단어는 development 이다. d가 들어간 단어가 많은데 Development 인지 design 인지 구분해야한다. TDD 는 development 이므로 TDD로 넘어가기 전에 설계(design) 을 끝내야 한다.
design에 대한 고민을 안하고 TDD 를 하려고 하면 어렵다. TDD를 사용하면 자신감을 불어 넣어 준다.
TDD = TFD(Test First Development) + 리팩터링
클래스 설계, 명세, 기능 요구사항 분석은 사전에 수행
- TDD의 사이클
- 실패 테스트 작성
- 테스트가 빨리 통과 되게끔 만든다. -> 죄악을 저지른다. 밑에 나오는 예시를 보면 죄악이 어떤건지 알게될 것이다ㅎㅎ
- 리팩터링 -> 코드와 테스트 코드 리팩터링
처음엔 jump 하지 마라 나중엔 간단한 단계는 점프하는데 이를 quantum jump 라고 한다.
코드 변경에 대한 근거를 만드는 과정이다.
테스트 생성의 구조는 다음과 같다.
//given
환경
//when
실행
//then
결과
자동차 경주 게임 예시를 통해 TDD를 알아보자!
https://github.com/woowacourse/kotlin-lotto
자동차 경주를 하기 위해서는 Car class가 필요할 것이다. 자동차는 다음과 같은 기능을 가진다고 해보자.
- 자동차는 이름을 가진다.
- 자동차의 이름은 1글자 ~ 5글자 이다.
이 기능을 봤을 때 TDD 사이클을 따라가기 위해 먼저 테스트를 작성한다.
자동차는 이름을 가진다.
@Test
fun `자동차는 이름을 가진다`(){
val car = Car()
assertThat(car.name).isEqualTo("seong")
}
이 테스트를 통과하기 위해 빠르게 통과하기 위해 죄악을 저지른다.
class Car {
val name = "seong"
}
테스트가 통과~
다음 단계로 이게 정말 맞는지 테스트를 추가하여 확인한다.
@Test
fun `자동차는 이름을 가진다 1`(){
val car = Car()
assertThat(car.name).isEqualTo("seong")
}
@Test
fun `자동차는 이름을 가진다 2`(){
val car = Car()
assertThat(car.name).isEqualTo("hoon")
}
테스트를 돌리면 당연히 fail 한다.
test 가 fail 했다는 것은 즉 Car 를 고쳐야 할 근거가 생겼다는 것이다.
class Car (val name: String)
Car 가 name을 받을 수 있도록 생성자를 수정한다.
@Test
fun `자동차는 이름을 가진다 1`(){
val car = Car("seong")
assertThat(car.name).isEqualTo("seong")
}
@Test
fun `자동차는 이름을 가진다 2`(){
val car = Car("hoon")
assertThat(car.name).isEqualTo("hoon")
}
다음과 같이 두 가지 테스트에 통과하게 된다~
더 이상 코드를 리팩터링 할 게 없다면 테스트 코드를 리팩터링 해야한다. 같은 형태의 테스트에 값만 바뀌는 것을 @ParameterizedTest 로 한번에 처리할 수 있다.
class CarTest{
@ValueSource(strings = ["seong", "hoon", "jeong", "jaino"])
@ParameterizedTest
fun `자동차는 이름을 가진다`(name: String){
val car = Car(name)
assertThat(car.name).isEqualTo(name)
}
}
다음 기능을 구현해 보자
자동차 이름은 1글자 ~ 5글자 이다.
다음과 같이 테스트를 작성할 수 있다.
@Test
fun `자동차 이름은 1글자 이상 5글자 이하여야 한다`() {
assertThrows<IllegalArgumentException> { Car("seonghoon") }
}
테스트를 통과하기 위해서 죄악을 저지른다!
단, 여기서 주의할 점은 이전 테스트를 통과하게 죄악을 저질러야 한다는 것이다.
아래와 같이 짜면 기존의 테스트를 통과하지 못한다.
class Car (val name: String){
init{
throw IllegalArgumentException()
}
}
다음과 같이 조건문을 추가하여준다.
class Car (val name: String){
init{
if (name.length in 1..5) throw IllegalArgumentException()
}
}
테스트 통과!
이제 코드 리팩토링 단계이다.
kotlin 의 require 을 사용하면 다음과 같이 변경 가능하다.
require() 은 내부에 조건을 만족하지 않으면 illegalArgumentException 을 발생시킨다.
class Car (val name: String){
init{
require(name.length in 1..5)
}
}
예외가 발생했으니 예외 메시지를 넣어 주는 것이 좋겠다.
kotlin 의 companion object 를 사용한다.
class Car(val name: String) {
init {
require(name.length in 1..5) { NAME_LENGTH_ERROR }
}
companion object {
private const val NAME_LENGTH_ERROR = "이름은 1글자 이상 5글자 이하여야 합니다"
}
}
더 하고 싶은 게 있을까?
매직 넘버를 사용하지 않고 범위 등을 설정할 때 숫자를 상수화 해주는 것이 좋다.
class Car(val name: String) {
init {
require(name.length in MINIMUM_NAME_LENGTH..MAXIMUM_NAME_LENGTH) { NAME_LENGTH_ERROR }
}
companion object {
private const val MINIMUM_NAME_LENGTH = 1
private const val MAXIMUM_NAME_LENGTH = 5
private const val NAME_LENGTH_ERROR = "이름은 1글자 이상 5글자 이하여야 합니다"
}
}
요구사항을 만족하면서 클린한 코드 작성이 가능하다.
'우아한테크코스' 카테고리의 다른 글
블랙잭 상태 패턴 구현 : [우아한테크코스 5기 AN_베르] (0) | 2023.04.10 |
---|---|
로또 미션 피드백 : [우아한테크코스 5기 AN_베르] (0) | 2023.02.27 |
Lv1 자동차 경주 미션 피드백 2 : [우아한테크코스 5기 AN_베르] (0) | 2023.02.14 |
Lv1 자동차 경주 미션 피드백 1 : [우아한테크코스 5기 AN_베르] (0) | 2023.02.13 |
코틀린 이해하기 : [우아한테크코스 5기 AN_베르] (1) | 2023.02.08 |