본문 바로가기
ETC/우아한테크코스 3기

랜덤에 대한 테스트는 어떻게 이루어 져야 하는가?

by 손너잘 2021. 2. 8.

테스트 코드를 작성하다 보면, 랜덤하게 발생하는 경우에 대하여 테스트를 진행할 경우가 생긴다.

이러한 상황에 봉착했을 때 우리는 어떻게 테스트 하는게 좋을까??

 

우아한 테크 코스(이하 우테코)에서 1단계 미션을 진행하면서 랜덤을 테스트 할 경우가 생겼다

 

아래의 코드를 보자.

 

public class Car {
    private static final int MAX_BOUND = 9;
    private static final int MOVABLE_THRESHOLD = 4;

    private final String name;
    private int position;

    public Car(String name, int position) {
        this.name = name;
        this.position = position;
    }

    public void move() {
        if (!isMovable()) {
            return;
        }

        position++;
    }

    private boolean isMovable() {
        return MOVABLE_THRESHOLD < RandomNumber.generate(MAX_BOUND);
    }
}

RandomNumber 객체가 0~9까지의 랜덤한 숫자를 반환하고, 만일 그 값이 4가 넘으면 자동차의 position을 변경하는 코드이다. 이때 자동차의 move()가 랜덤한 값에 의해 움직이기 때문에 우리는 이에 대한 테스트를 진행하는데 어려움을 겪는다.

 

이에 대한 답을 찾기 위해 인터넷을 뒤적거리다가, 우테코 선배와 포비(자바지기)의 글을 보게 되었다.

pjh3749.tistory.com/242

https://www.slipp.net/questions/557

 

Random 값을 처리하는 코드는 어떻게 단위 테스트할 수 있을까?

요구사항은 다음과 같다. Random 값을 구한 후 Random 값이 4이상인 경우 자동차를 1만큼 증가시킨다. 이 같은 요구사항을 만족하기 위한 구현 코드를 다음과 같이 구현할 수 있다. public class Car { priva

www.slipp.net

정말 좋은 글이다. RandomGenerator와 Car의 강한 결합을 분리하여 테스트를 진행할 수 있도록 한다.

한번 시도해 보자.

 

아래와 같이 랜덤한 수를 생성하는 객체(RandomNumber)를 RandomUtil 인터페이스의 구현체로 만들고, 이를 Car에게 주입시키도록 구조를 바꾸었다.

public interface RandomUtil {
    int generate(int bound);
}
public class RandomNumber implements RandomUtil {
    private static final Random random = new Random();

    @Override
    public int generate(int bound) {
        return random.nextInt(bound + 1);
    }
}
public class Car {
    private static final int MAX_BOUND = 9;
    private static final int MOVABLE_THRESHOLD = 4;

    private RandomUtil randomUtil;
    private final String name;
    private int position;

    public Car(String name, int position, RandomUtil randomUtil) {
        this.name = name;
        this.position = position;
        this.randomUtil = randomUtil;
    }

    public void move() {
        if (!isMovable()) {
            return;
        }

        position++;
    }

    private boolean isMovable() {
        return MOVABLE_THRESHOLD < randomUtil.generate(MAX_BOUND);
    }
}

이를 통해서 우리는 아래와 같이 테스트 코드를 작성할 수 있다.

public class CarTest {
    @Test
    public void carTest() {
        Car car = new Car("손너잘", 1, bound -> 3);
        assertThat(car.getPosition()).isEquals(1);

        car = new Car("손너잘", 1, bound -> 5);
        assertThat(car.getPosition()).isEquals(2);
    }
}

 

 

 

여기까지는 좋았다. 하지만, 문득 이런 생각이 들었다. "생성되는 랜덤 넘버는 결국 테스트 하는 '사람'에 의해 만들어지는 건데, 실제로는 RandomUtil이 0~9의 바운드가 아니라 -1을 만들수도 있는 것 아닌가? 그러면 이 테스트에 의미가 있나? RandomUtil의 잘못된 바운드를 검증하지 못하는데.."

 

이러한 생각에 슬랙에 질문을 올렸었다.

질문

랜덤 테스트를 위해 반목문을 돌려야 하는가부터 여러 논의가 오간 끝에, 결국 나의 결론은 아래와 같이 났다.

그렇게 랜덤 숫자의 바운드 테스트는 안하는걸로 결론이 났었는데...

 

리팩토링을 하다보니 살짝 미심쩍긴 하지만 어느정도 테스트 할 방법을 찾은 것 같다. 정말 간단한건데 왜 고민했지? 라는 생각이 든다.

 

첫번째는, 비즈니스 로직에서 생성한 랜덤값을 테스트 하는 것 이다.

private boolean isMovable() {
    int randomNumber = randomUtil.generate(MAX_BOUND);

    if(0 <= randomNumber && randomNumber <= MAX_BOUND) {
        return MOVABLE_THRESHOLD < randomUtil.generate(MAX_BOUND);
    }

    throw new IllegalArgumentException();
}

이와 같이 randomNumber를 사용하는 곳에서 바운드 체크를 하는 validation 로직을 넣으면, 아래와 같이 테스트가 가능하다.

public class RandomMoveConditionTest {
    @ParameterizedTest
    @ValueSource(ints = {10, 11, 12})
    @DisplayName("전진조건이 0-9사이 값이 아닌 경우 예외")
    public void 전진조건이_0_9_사이의_값이_아닌_경우_예외(int randomNumber) {
        assertThatExceptionOfType(MoveConditionOutOfBoundException.class)
                .isThrownBy(
                        () -> new Car("손너잘", 0, randomNumber).move()
                ).withMessageContaining(new MoveConditionOutOfBoundException().getMessage());
    }
}

설계의 관점에서 바라보더라도, 랜덤 유틸은 단순히 랜덤한 숫자를 반환하는 역할을 부여하고 그 값에 대한 검증은 도메인에서 자신들의 명세를 가지고 하는게 이상하진 않다.

 

하지만 실제 코드에서 이러한 방식으로 테스트했다가 테스트는 통과하는데 코드에는 문제가 있던 경우를 몸소 느꼈다. 아래와 같은 경우였다.

public boolean isMovable() {
    int randomNumber = randomUtils.generate();

    validateTRandomNumberBound(randomNumber);

    return randomUtils.generate() >= MOVABLE_BOUND;
}

코드를 리팩토링 하다가 실수한 부분이다. 도메인쪽에 validation을 넣었는데, return에서 새로운 랜덤넘버를 생성하고 있다. 이 경우 return구문의 randomUtils가 0~9 바운더리보다 더 크거나 작은 값을 생성하면, validation에서는 통과했지만 randomUtils는 적절하지 못한 행동을 한것이 된다.

 

이러방 상황을 방지하기 위한 방법이 있을까?

public class RandomNumber implements RandomUtil {
    private static final Random random = new Random();

    @Override
    public int generateBound(int bound) {
        int randomNumber = random.nextInt(bound + 1);

        if (randomNumber > bound) {
            throw new IllegalArgumentException();
        }

        return randomNumber;
    }
}

이는 RandomNumber 객체에게 단순히 랜덤 값을 생성하는 역할을 줬다고 생각한게 아니라, 특정 바운더리의 랜덤값을 생성한다는 역할을 부여했다 생각하고 작성한 것이다.

 

하지만 이러한 코드는 테스트가 불가하며 라이브러리에 대한 도전으로 보여진다.

 

사실 이 이상으로는 아무리 생각해도 방법이 안떠올라서 그냥 첫번째 방법을 선택했다. 코더가 조금만 더 주의하면 되지 않을까 하는 생각에...

 

사실 기능면으로만 보면 큰 문제는 아니다(자동차를 움직인다는 기능에 한하여). 요구사항은 MOVALBE_BOUND보다 숫자가 크거나 작으면 OK라고 하였으니.

하지만 이러한 부분을 세세하게 보지 않고 스윽 넘어가는 행위는 결국엔 아래와 같은 상황을 만들어 내는 시발점이라고 생각한다.

 

돌아가긴 하네..

댓글