우아한테크코스

TDD : [우아한테크코스 5기 AN_베르]

베르_최성훈 2023. 2. 15. 13:52

테스트 주도 개발 (Test-driven development TDD)

 여기서 가장 중요한 단어는 development 이다. d가 들어간 단어가 많은데 Development 인지 design 인지 구분해야한다. TDD 는 development 이므로 TDD로 넘어가기 전에 설계(design) 을 끝내야 한다. 

 design에 대한 고민을 안하고 TDD 를 하려고 하면 어렵다. TDD를 사용하면 자신감을 불어 넣어 준다.

TDD = TFD(Test First Development) + 리팩터링

 

클래스 설계, 명세, 기능 요구사항 분석은 사전에 수행

 

  • TDD의 사이클
    1. 실패 테스트 작성
    2. 테스트가 빨리 통과 되게끔 만든다. -> 죄악을 저지른다. 밑에 나오는 예시를 보면 죄악이 어떤건지 알게될 것이다ㅎㅎ
    3. 리팩터링 -> 코드와 테스트 코드 리팩터링

처음엔 jump 하지 마라 나중엔 간단한 단계는 점프하는데 이를 quantum jump 라고 한다.

코드 변경에 대한 근거를 만드는 과정이다.

 

테스트 생성의 구조는 다음과 같다.

//given
환경
//when 
실행
//then
결과

 

자동차 경주 게임 예시를 통해 TDD를 알아보자!

 

https://github.com/woowacourse/kotlin-lotto

 

GitHub - woowacourse/kotlin-lotto

Contribute to woowacourse/kotlin-lotto development by creating an account on GitHub.

github.com

 

자동차 경주를 하기 위해서는 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글자 이하여야 합니다"
    }
}

 

요구사항을 만족하면서 클린한 코드 작성이 가능하다.