자동차 경주 게임 구현 - 테스트 코드에서의 팁

PPakSSam·2022년 1월 18일
1
post-thumbnail

순서

  1. 자동차 경주 게임 구현 - Code Convention
  2. 자동차 경주 게임 구현 - 객체를 객체스럽게 리팩토링 하자
  3. 자동차 경주 게임 구현 - 비즈니스 로직과 UI의 분리
  4. 자동차 경주 게임 구현 - 테스트 코드에서의 팁

이 포스트는 자바 플레이그라운드 with TDD, 클린코드를 들었다는 전제하에 쓴 글이며,
수업을 듣지 않았어도 전체 깃헙코드를 보고 어느정도 안다는 전제하에 쓴 글이다.


1. 경계값을 기준으로 테스트한다.

자동차 경주 게임의 요구사항에는 1~9까지의 랜덤 숫자중 4이상이 나오면 자동차가 움직인다가 있다. 그렇다면 여기서 경계값은 4이다.
자바지기 박재성님에 의하면 경계값에서 버그가 많이 일어난다고 한다.
따라서 모든 값을 테스트할 필요는 없고 경계값을 기준으로 테스트하면 된다고 하신다.

@Test
public void 랜덤숫자가_4이상일때만_움직인다() {
    Car car = new Car();
    
    assertFalse(car.shouldMove(0));
    assertFalse(car.shouldMove(1));
    assertFalse(car.shouldMove(2));
    assertFalse(car.shouldMove(3));
    
    assertTrue(car.shouldMove(4));
    assertTrue(car.shouldMove(5));
    assertTrue(car.shouldMove(6));
    assertTrue(car.shouldMove(7));
    assertTrue(car.shouldMove(8));
    assertTrue(car.shouldMove(9));
}

이렇게 모든 값을 테스트하는 것은 낭비이고

@Test
public void 랜덤숫자가_4이상일때만_움직인다() {
    Car car = new Car();

    assertFalse(car.shouldMove(3));
    assertTrue(car.shouldMove(4));
}

이렇게 경계값을 기준으로 테스트하면 된다고 한다.

2. Test Fixture 생성

Fixture란 테스트를 실행하기 위해 필요한 것으로 테스트를 실행하기 위해 준비해야할 것들을 의미한다. 테스트의 인스턴스 변수각 Test Case에서 공통으로 필요한 Fixture만 위치하고, 나머지는 각 Test Case에 로컬 변수로 구현한다.

public class RacingGameTest {
    private final String[] testNames = {"a","b","c"};
    private final int testPosition = 5;
    private final String resultSamePositionString = ", b";
    private final String resultWinnersString = "a, b";
    RacingGame racingGame;
    private final Car firstWinner = new Car(testPosition, "a");
    private Car secondWinner;
    private final List<Car> cars = new ArrayList<Car>();
    [...]
}

이런식으로 테스트 케이스에 필요한 모든 변수들을 인스턴스 변수로 선언하면 좋지 않다.

public class StringCalculatorTest {
    private StringCalculator emptyInputStringCalculator;
    private StringCalculator whiteSpaceInputStringCalculator;
    private StringCalculator nullInputStringCalculator;
    private StringCalculator stringCalculator;
    private StringCalculator minArrayLengthStringCalculator;
    private StringCalculator evenArrayLengthStringCalculator;
    ...
    
    @BeforeEach
    void setUp() {
        System.out.println("StringCalculator Test setUp");
        emptyInputStringCalculator = new StringCalculator("");
        whiteSpaceInputStringCalculator = new StringCalculator(" ");
        nullInputStringCalculator = new StringCalculator(null);
        stringCalculator = new StringCalculator("2 + 3 * 4 / 2");
        minArrayLengthStringCalculator = new StringCalculator("2 +");
        minArrayLengthStringCalculator.splitInputString();
        evenArrayLengthStringCalculator= new StringCalculator("2 + 3 *");
        evenArrayLengthStringCalculator.splitInputString();
        ...
    }
}

또한 @BeforeEach에서 모든 Fixture를 초기화할 필요가 없으며 중복으로 사용하는 Fixture만 초기화하는 것이 좋다.

3. 특정 상태를 만들기 위한 반복 코드 없애는 법

우승자 구하는 로직을 테스트하기 위해 Test Fixture를 준비하는 코드이다.

public class RacingGameResultTest {
    @Test
    public void check_ranking_if_correct() {
        List<Car> cars = new ArrayList<>();
        Car car1 = new Car("pobi");
        Car car2 = new Car("crong");
        Car car3 = new Car("honux");
        
        car1.move();
        car1.move();
        car2.move();
        car2.move();
        car2.move();
        car3.move();

        cars.add(car1);
        cars.add(car2);
        cars.add(car3);
        [...]
    }
}

위와 같이 자동차들의 포지션을 변경하기 위해 move()를 계속 호출하는 것은 좋지 않아 보인다. 이러한 중복코드를 없애기 위해 Car(String name, int position) 생성자를 추가한다면

public class RacingGameResultTest {
    @Test
    public void check_ranking_if_correct() {
        List<Car> cars = Arrays.asList(new Car("pobi", 2), 
                                       new Car("crong", 3), 
                                       new Car("honux", 1));
        [...]
    }
}

위와 같이 깔끔한 코드를 짤 수 있게 된다.

4. 테스트 하기 어려운 부분을 테스트 가능하게 바꾸는 법

다음 코드는 Random 때문에 단위테스트를 하기 힘들다.

public class RandomEngine {

    private static final int MIN_VALUE = 0;
    private static final int MAX_VALUE = 9;

    public boolean moveable() {
        return RandomUtils.nextInt(MIN_VALUE, MAX_VALUE) > MOVABLE_DIGIT;
    }
}

public class Car {
	RandomEngine randomEngine;
    
    public void move() {
        if(randomEngine.moveable()) {
            this.position = position.moveForward(GO_FORWARD_DISTANCE);
        }
    }
}

그렇다면 위의 코드를 단위테스트가 가능하게 리팩토링한다면 어떻게 하는것이 좋을까?

1. RandomEngine을 interface로 추상화한다.

public interface Engine {

    int MOVABLE_DIGIT = 3;

    boolean moveable();
}

public class RandomEngine implements Engine{

    private static final int MIN_VALUE = 0;
    private static final int MAX_VALUE = 9;

    @Override
    public boolean moveable() {
        return RandomUtils.nextInt(MIN_VALUE, MAX_VALUE) > MOVABLE_DIGIT;
    }
}

2. 테스트 코드에서는 테스트 가능한 Engine을 구현하여 사용한다.

public class FixedEngine implements Engine {

    private int num;

    public FixedEngine(int num) {
        this.num = num;
    }

    @Override
    public boolean moveable() {
        return num > MOVABLE_DIGIT;
    }
}

public class CarTest {

    @Test
    @DisplayName("자동차 움직임 및 현재상태 테스트")
    public void moveTest() throws Exception {

        // given
        Car movableCar = new Car("ppak", new FixedEngine(4));
        Car immovableCar = new Car("ppak", new FixedEngine(3));

        // when
        int numOfRacingRound = 5;
        for (int i = 0; i < numOfRacingRound; i++) {
            movableCar.move();
            immovableCar.move();
        }

        // then
        Assertions.assertThat(movableCar.getPosition()).isEqualTo(Position.of(5));
        Assertions.assertThat(immovableCar.getPosition()).isEqualTo(Position.of(0));
    }

}
profile
성장에 대한 경험을 공유하고픈 자발적 경험주의자

0개의 댓글