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

3 분 소요

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

Production Code

프로덕션 코드는 프로그램 구현을 담당하는 부분으로, 사용자가 실제로 사용하는 코드이다.

public class Calculator{
    int add(int i, int j){
        return i + j;
    }
    int subtract(int i, int j){
        return i - j;
    }
    int multiply(int i, int j){
        return i * j;
    }
    int divide(int i, int j){
        return i / j;
    }
}

Test Code

테스트 코드는 프로덕션 코드가 정상적으로 동작하는지를 확인하는 코드이다.

public static void main(String[] args){
    Calculator cal = new Calculator();
    System.out.println(cal.add(3,4));
    System.out.println(cal.subtract(5,4));
    System.out.println(cal.multiply(2,6));
    System.out.println(cal.divide(8,4));
}

TDD란?

  • TDD = TFD(Test First Development) + 리팩토링
  • TDD란 프로그래밍 의사결정과 피드백 사이의 간극을 의식하고 이를 제어하는 기술이다.
  • TDD의 아이러니 중 하나는 테스트 기술이 아니라는 점이다. TDD는 분석 기술이며, 설계 기술이기도 하다.

TDD를 하는 이유

  • 디버깅 시간을 줄여준다.
  • 동작하는 문서 역할을 한다.
  • 변화에 대한 두려움을 줄여준다.

TDD 사이클

Test fails -> Test passes -> Refactor

  • 실패하는 테스트를 구현한다.
  • 테스트가 성공하도록 프로덕션 코드를 구현한다.
  • 프로덕션 코드와 테스트 코드를 리팩토링한다.

TDD 원칙

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

도메인 지식, 객체 설계 경험이 있는 경우

  • 요구사항 분석을 통해 대략적인 설계 - 객체 추출
  • UI, DB 등과 의존관계를 가지지 않는 핵심 도메인 영역을 집중 설계

막막하다면?

  • 단위 테스트도 없고, TDD도 아니고, 객체 설계도 하지 않고, 기능 목록을 분리하지도 않고 지금까지 익숙한 방식으로 일단 구현
  • 구현하려는 프로그래밍의 도메인 지식을 쌓는다.
  • 구현한 모든 코드를 벌니다.
  • 구현할 기능 목록 작성 또는 간단한 도메인 설계
  • 기능 목록 중 가장 만만한 녀석부터 TDD로 구현 시작
  • 복잡도가 높아져 리팩토링하기 힘든 상태가 되면 다시 버린다.
  • 다시도전

TDD로 숫자 야구게임 구현 첫번째

기능 목록을 작성한 후 테스트 가능한 부분을 찾아 TDD로 도전

  • 1 ~ 9의 숫자 중 랜덤으로 3개의 숫자를 구한다
  • 사용자로부터 입력 받는 3개 숫자 예외 처리
    • 1 ~ 9 의 숫자인가
    • 중복 값이 있는가?
    • 3자리인가?
  • 위치와 숫자 값이 같은 경우 - 스트라이크
  • 위치는 다른데 숫자 값이 같은 경우 - 볼
  • 숫자 값이 다른 경우 - 낫싱
  • 사용자가 입력한 값에 대한 실행 결과를 구한다

1단계 - Util 성격의 기능이 TDD로 도전하기 좋음

  • 사용자로부터 입력 받는 3개 숫자 예외 처리
    • 1 ~ 9 의 숫자인가
    • 중복 값이 있는가?
    • 3자리인가?

      2단계 - 테스트 가능한 부분에 대해 TDD로 도전

  • 위치와 숫자 값이 같은 경우 - 스트라이크
  • 위치는 다른데 숫자 값이 같은 경우 - 볼
  • 숫자 값이 다른 경우 - 낫싱

테스트코드

public class BallsTest {

    @Test
    @DisplayName("nothing")
    public void nothing(){
        Balls answer = new Balls(Arrays.asList(1,2,3)); //answer
        PlayResult playResult = answer.play(Arrays.asList(4,5,6));
        assertThat(playResult.getStrike()).isEqualTo(0);
        assertThat(playResult.getBall()).isEqualTo(0);
    }

    @Test
    @DisplayName("1ball")
    public void play_1ball(){
        Balls answer = new Balls(Arrays.asList(1,2,3)); //answer
        PlayResult playResult = answer.play(Arrays.asList(2,4,5));
        assertThat(playResult.getBall()).isEqualTo(1);
        assertThat(playResult.getStrike()).isEqualTo(0);
    }

    @Test
    @DisplayName("1ball 1strike")
    public void play_1ball_3strike(){
        Balls answer = new Balls(Arrays.asList(1,2,3)); //answer
        PlayResult playResult = answer.play(Arrays.asList(2,4,3));
        assertThat(playResult.getBall()).isEqualTo(1);
        assertThat(playResult.getStrike()).isEqualTo(1);
    }
}

Balls 클래스

public class Balls {
    private final List<Ball> answers;
    
    public Balls(List<Integer> arrayBalls) {
        List<Ball> balls =  mapTo(arrayBalls);
        this.answers = balls;
    }
    //정수 타입의 리스트를 Ball 타입의 리스트로 변환해주는 메서드
    private List<Ball> mapTo(List<Integer> arrayBalls) {
        List<Ball> balls= new ArrayList<>();
        for(int i=0; i<arrayBalls.size(); i++){
            balls.add(new Ball(i, arrayBalls.get(i)));
        }
       return balls;
    }
    //전체 play 결과를 반환하는 메서드
    public PlayResult play(List<Integer> balls){
        Balls userBalls = new Balls(balls);
        PlayResult result = new PlayResult();
        for (Ball answer : answers){
            BallStatus status = userBalls.play(answer);
            result.report(status);
        }
        return result;
    }
    //각 Ball의 결과 상태(BallSatus)를 구하는 메서드
    public BallStatus play(Ball userBall) {
        return answers.stream()
                .map(answer -> answer.play(userBall))
                .filter(BallStatus::isNotNothing)
                .findFirst()
                .orElse(BallStatus.NOTHING);
    }
}

play 결과를 담을 클래스

public class PlayResult {

    private int strike = 0;
    private int ball = 0;


    public void report(BallStatus status){
        if(status.isStrike()){
            this.strike +=1;
        }
        if(status.isBall()){
            this.ball +=1 ;
        }
    }

    public int getStrike() {
        return this.strike;
    }

    public int getBall() {
        return this.ball;
    }

    public Boolean isGameEnd(){
        return this.strike ==3;
    }
}

enum타입의 BallStatus

public enum BallStatus {

    NOTHING, BALL, STRIKE;

    public boolean isStrike() {
        return this == STRIKE;
    }
    public boolean isBall(){
        return this == BALL;
    }

    public boolean isNotNothing() {
        return this != NOTHING;
    }
}

태그: ,

카테고리:

업데이트: