본문 바로가기
우아한테크코스 4기/레벨1

[Java] 대역으로 Junit 테스트 코드 작성하기

by 나는후니 2022. 2. 15.

지난 포스팅에서 Random 값을 테스트 할 수 있는 StubNumberGenerator 를 만들어 간단하게 테스트를 진행해보았다. 이번 포스팅에서는 레벨 1 미션의 랜덤값에 의한 테스트를 자유자재로 할 수 있는 스텁 객체를 만들고자 한다.

Stub 이란?
Stub이란 구현을 단순한 것으로 대체한 것이다. 테스트에 맞게 단순히 원하는 동작을 수행해준다. 대역 테스트에는 실제 동작을 구현하는 Fake, 호출된 내용을 기록하는 Spy, 기대값을 반환 받는 Mock이 있다. 이번 테스팅은 단순히 원하는 동작을 수행해준다는 의미에서 Stub이라고 접두사를 붙였다.


처음 Stub 객체를 작성할 때는 내가 작성한 테스트가 동작하기 위한 객체를 만들었다.
public class StubNumberGenerator implements NumberGenerator{
    private static final int NOT_MOVABLE = 3;
    private static final int MOVABLE = 4;

    // 반복문을 도는 순서대로 1, 2번 인덱스를 거치는 객체가 앞으로 움직이게끔 세팅했다.
    private int[] numbers = new int[]{NOT_MOVABLE, MOVABLE, MOVABLE, NOT_MOVABLE};
    private int sequence = 0;

    @Override
    public int generate() {
        if (sequence == 4) {
            sequence = 0;
        }
        return numbers[sequence++];
    }
}

아래 코드에서 위 클래스를 사용할 수 있다.
@Test
void 자동차_우승_2명_성공() {
    Cars cars = new Cars(new String[]{"jae", "rick"});
    for (int i = 0; i < 2; i++) {
        cars.moveCars(numberGenerator); // StubNumberGenerator가 들어간다.
    }
    assertThat(cars.getWinners()).isEqualTo("jae, rick가 최종 우승 했습니다.");
}

하지만 이 코드에는 큰 문제점이 있다. 억지로 해당 테스트만 통과할 수 있는 가짜 객체를 만든 것이다. 그리고 리뷰어분께서 그 문제점을 정말 잘 지적해주셨다.

Stub 객체가 배열의 sequence에 의존하기 때문에 우승자가 없을 때, 3명 중 2명 우승 등 다양한 상황을 테스트하는 것이 무리가 있던 것이다. 이 문제를 해결하기 위해 이전 포스팅에서 작성한대로 격리를 통해 Stub 객체를 사용하지 않고 해결하는 방법이나 Mockito 라이브러리를 사용하는 방법이 있겠으나 충분히 대역을 통해 문제를 해결할 수 있었다.

StubNumberGenerator

먼저 어떻게 테스트를 진행할지 고민해봐야 한다. 이 클래스를 만들면서 고민한 과정은 다음과 같다.


  1. 코드가 실행되어 numberGenerator의 generate 메서드가 실행될 때 원하는 숫자가 return 되면 좋겠다.
  2. 원하는 숫자가 return 되기 위해서는 반복문의 횟수만큼 원하는 숫자를 입력해야 한다.
  3. 반복문이 진행되면서 내가 입력한 숫자 리스트의 index값이 순차적으로 증가하면 원하는 코드를 작성할 수 있다.

이 세 과정 끝에 코드를 작성할 수 있었다. 먼저 리스트를 만들어 리스트에 원하는 숫자를 넣고, 인덱스를 증가시키며 해당 인덱스의 숫자 값을 리턴해주는 코드를 작성했다.

하지만 이 때 고려해야 하는 예외 사항이 몇가지 있었다.


  1. 반복문의 회수만큼 입력값이 들어오는가?
  2. 입력된 값이 0 ~ 9 인가?
  3. 인덱스가 증가하는 과정에서 리스트의 크기보다 큰 인덱스가 들어오는가?

핵심 메서드는 아래와 같다. 이 메서드를 통해 반복문이 돌아가는 회수와 원하는 리턴값을 순서대로 기입할 수 있다.

public void prepareStubNumbers(int count, int ... values) {
    // 입력된 value의 개수가 count와 같은지 확인한다.
    checkValuesLengthSameAsCount(count, values);
    Arrays.stream(values)
            .forEach(value -> {
                // 값이 0 ~ 9인지 확인한다.
                checkValueRange(value);
                numbers.add(value);
            });
}

전체 코드

public class StubNumberGenerator implements NumberGenerator{

    private final List<Integer> numbers;
    private int index;

    // 1. 인스턴스 생성과 동시에 numbers와 index 값이 초기화 된다.
    public StubNumberGenerator() {
        numbers = new ArrayList<>();
        index = 0;
    }

    // 2. 입력한 값이 리스트에 들어간다.
    public void prepareStubNumbers(int count, int ... values) {
        checkValuesLengthSameAsCount(count, values);
        Arrays.stream(values)
                .forEach(value -> {
                    checkValueRange(value);
                    numbers.add(value);
                });
    }

    private void checkValuesLengthSameAsCount(int count, int[] values) {
        if (values.length != count) {
            throw new AssertionError("put as many values as the count");
        }
    }

    private void checkValueRange(int value) {
        if (value < 0 || value >= 10) {
            throw new AssertionError("put value that is between 0 and 9");
        }
    }

    @Override
    public int generate() {
        if (index >= numbers.size()) {
            throw new AssertionError("index out of bounds");
        }
        // 반복문을 돌며 generate가 실행될 때마다 index가 증가하며 해당 index의 값을 가져온다.
        return numbers.get(index++);
    }
}

Stub 객체를 이용하여 아래와 같이 자유롭게 테스트를 진행할 수 있다.
public class RacingControllerTest extends IOTest {

    @ParameterizedTest
    @ValueSource(strings = "ac,cd,ef\n2")
    void 레이싱_컨트롤러_플레이_성공(String input) {
        systemIn(input);
        RacingController racingController = new RacingController();
        StubNumberGenerator numberGenerator = new StubNumberGenerator();
        numberGenerator.prepareStubNumbers(6,3,3,4,4,3,3);

        racingController.play(numberGenerator);

        assertThat(getOutput()).contains("ac, ef가 최종 우승 했습니다.");
    }

    @ParameterizedTest
    @ValueSource(strings = "ac, cd,ef\nac,cd\n2")
    void 레이싱_컨트롤러_공백_에러_메시지(String input) {
        systemIn(input);
        RacingController racingController = new RacingController();
        StubNumberGenerator numberGenerator = new StubNumberGenerator();
        numberGenerator.prepareStubNumbers(4,4,1,4,1);

        racingController.play(numberGenerator);

        assertThat(getOutput()).contains("[ERROR] 이름에 공백이 존재합니다.")
                .contains("ac가 최종 우승 했습니다.");
    }

}

테스트 결과

입력값 대로 결과가 잘 출력되는 것을 확인할 수 있다.

정리

이번 포스팅을 통해 Stub 객체를 만들어 Test하는 방법을 알아보았다. 이 방법이 좋은 방법일지 확신할 수 없지만 직접 고민하고 코드를 작성하여 원하는 테스트를 짰다는 것이 굉장히 의미있다고 생각한다. 리뷰어분께 2단계 미션으로 리뷰를 다시 받고 이 방법에 대해 이야기를 나눈 뒤 정리 내용을 추가하겠다!