Test Driven Development(테스트 주도 개발, TDD)
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;
}
}