본문 바로가기

기타

TDD

 

TDD 란?

Test Driven Development(테스트 주도 개발, TDD)

테스트를 주도로 개발을 하는 개발 방식으로, TDD 의 단계는 다음과 같다.

1 단계

실패하는 테스트를 만든다.

2 단계

테스트가 성공하도록 프로덕션 코드를 구현한다.

3 단계

프로덕션 코드와 테스트 코드를 리펙토링한다.

그림으로 표현하면 다음과 같다.


TDD 원칙

  • 원칙 1 - 실패하는 단위 테스트를 작성할 때까지 프로덕션 코드(production code)를 작성하지 않는다.
  • 원칙 2 - 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
  • 원칙 3 - 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

 

⌨️TDD 해보기

다음 간단한 프로그램을 TDD 방식으로 구현해보자

예제 소개

** 문자열 계산기**

- 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환
(예: “” => 0, "1,2" => 3, "1,2,3" => 6, “1,2:3” => 6)

- 문자열 계산기에 숫자 이외의 값 또는 음수를 전달하는 경우 RuntimeException 예외를 throw 한다.

0. 기능 구현 리스트

TDD 에 익숙하지 않은 사람이라면 어디서 어떻게 시작해야 할지 막막할 수 있다.

그럴 땐 먼저 기능 구현 리스트를 세심하게 작성해보는 것을 추천한다.

- String 으로 문자열이 입력되면 쉼표( , ) 나 콜론( : ) 기준으로 split 하여 String[] 로 반환하는 기능

- 가지고 있는 원소들의 합을 구하는 기능

- 문자열에 숫자 이외의 값이 있으면 예외처리를 하는 기능

- 문자열에 음수가 있으면 예외처리를 하는 기능

...


개발 환경에 대한 참고사항

- Idle : Intellij

- Build 도구 : Gradle

Test 기능을 사용하기 위해 다음과 같이 dependencies 를 설정해 준다.

dependencies {
    testCompile('org.junit.jupiter:junit-jupiter:5.6.0')
    testCompile('org.assertj:assertj-core:3.15.0')
}

그렇다면 이제 본격적으로 기능 구현 리스트의 가장 위에 있는 split 하는 기능을 TDD 방식으로 구현해 보자.

1. 실패하는 테스트 만들기

만들고자 하는 Split의 기능은 String 이 들어왔을때 콤마( , ) 혹은 콜론( : ) 기준으로 나누어 String[] 으로 반환하는 기능이다.

먼저 콤마 기준으로 나누는 기능을 먼저 만들어보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package domin;
 
import domain.StringCalculator;
import org.junit.jupiter.api.Test;
 
import static org.assertj.core.api.Assertions.*;
 
public class StringCalculatorTest {
    @Test
    void split() {
        String input = "1,2,3";
        String[] result = new String[]{"1","2","3"};
        
        assertThat(StringCalculator.split(input)).isEqualTo(result);
    }
}

이렇게 하니 split 이라는 함수가 StringCalculator 에 없어서 컴파일 에러가 뜨는것을 확인할 수 있다.

컴파일에는 성공하는 테스트를 만들어야 하기 때문에 StringCalculator 에도 split 함수를 만들어 주었다.

1
2
3
4
5
6
7
8
package domain;
 
public class StringCalculator {
    public static String[] split(String input) {
        return null;
    }
}

이 상태에서 테스트를 실행하면.... 당연히 테스트에 실패하고 만다.

혹시라도 다음과 같은 에러가 발생한다면 인텔리제이 설정에 들어가서 표시한 부분을 gradle에서 인텔리제이로 변경해주면 된다.

> Task :testClasses
> Task :test FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':test'.

 

2. 테스트 성공 시키기

 

이제 실패하는 테스트를 만들었으니 TDD 사이클대로 테스트를 성공하도록 만들고 리펙토링하는 작업을 다음 글에서 해보자!

1
2
3
4
5
6
7
8
9
package domain;
 
public class StringCalculator {
    public static String[] split(String input) {
        String[] result = input.split(",");
        return result;
    }
}

다음과 같이 프로덕션 코드를 수정해주니 테스트가 정상적으로 통과하였다.

3. 리펙토링

1
2
3
4
5
6
7
8
package domain;
 
public class StringCalculator {
    public static String[] split(String input) {
        return input.split(",");
    }
}

다시 실패하는 테스트 만들기

콤마( : ) 기준으로도 split을 해야하기 때문에 다른 케이스를 추가해보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package domin;
 
import domain.StringCalculator;
import org.junit.jupiter.api.Test;
 
import static org.assertj.core.api.Assertions.*;
 
public class StringCalculatorTest {
    @Test
    void split() {
        String input = "1,2,3";
        String[] result = new String[]{"1","2","3"};
 
        assertThat(StringCalculator.split(input)).isEqualTo(result);
 
        input = "1,2:3";
        assertThat(StringCalculator.split(input)).isEqualTo(result);
    }
}

위의 코드로 테스트를 실행하면 역시나 또 다시 실패한다.

그럼 프로덕션 코드를 수정한다.

1
2
3
4
5
6
7
package domain;
 
public class StringCalculator {
    public static String[] split(String input) {
        return input.split(",|:");
    }
}

4. 다시 실패하는 테스트만들기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package domin;
 
import domain.StringCalculator;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
 
import static org.assertj.core.api.Assertions.*;
 
public class StringCalculatorTest {
    @DisplayName("',' 나 ':' 기준으로 split")
    @Test
    void split() {
        String input = "1,2,3";
        String[] result = new String[]{"1","2","3"};
 
        assertThat(StringCalculator.split(input)).isEqualTo(result);
 
        input = "1,2:3";
        assertThat(StringCalculator.split(input)).isEqualTo(result);
    }
 
    @DisplayName("입력된 배열의 합 구하기")
    @Test
    void sum() {
        String[] input = new String[]{"1","2","3"};
        
        assertThat(StringCalculator.sum(input)).isEqualTo(6);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
package domain;
 
public class StringCalculator {
    public static String[] split(String input) {
        return input.split(",|:");
    }
 
    public static int sum(String[] numbers) {
        return 0;
    }
}

테스트 코드도 틈틈이 리펙토링 해주어야 한다.

다시 테스트가 통과하도록 프로덕션 코드 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package domain;
 
public class StringCalculator {
    public static String[] split(String input) {
        return input.split(",|:");
    }
 
    public static int sum(String[] numbers) {
        int result = 0;
        for (String number : numbers) {
            result += Integer.parseInt(number);
        }
        return result;
    }
}

 

5. 반복

위와 같은 사이클을 반복하며 메소드를 하나씩 구현해 나가면 된다.

 

🧐TDD의 효과

  • 설계적 측면
    • 메서드의 input, output이 정해져야 테스트가 가능하므로 개발자로 하여금 구체적인 설계를 생각하도록 한다.
    • 테스트 자체가 어려울때 설계를 재검토하고 코드를 더 단순하게 리펙토링하게 만드는 효과가 있다.
  • 빠른 피드백을 얻을 수 있다. (ex. 버그를 일찍 발견할 수 있다.)
  • 테스트 코드 작성 능력이 향상 된다.
  • 테스트 코드 자체가 해당 API 에 대한 문서화가 된다.
  • 회귀 테스트 역할을 해준다.
반응형