Lecture/[NextStep] DDD 세레나데 6기

[DDD 세레나데 6기] 1주차 - 0단계 Junit5 학습

Seyun(Marco) 2024. 5. 17. 14:25
728x90

서론

  • DDD 세레나데의 첫번재 미션인 Junit5를 학습하는 시간입니다.
  • 지금까지 굉장히 많이 해본 자동차 경주게임의 Car 도메인을 아주 간단히 만들어 보는 미션으로 실제 테스트 코드까지 작성하는것이 목표였습니다.
  • 요구사항중에 중요한 부분은 아래와 같습니다.
    • 자동차 이름이 존재하며, 5글자를 넘을 수 없습니다.
    • 자동차가 움직이는 조건은 0~9사이의 무작위 값을 구한 뒤, 무작위 값이 4이상인 경우에 움직인다.
  • 이제 이걸 어떻게 코드를 작성했는지 간단히 이야기 해보도록 하겠습니다.
  • 관련PR

https://github.com/next-step/ddd-legacy/pull/573/files

Car

public record Name(String name) {
    private static final int MAX_NAME_LENGTH = 5;

    public Name {
        validateName(name);
    }

    private void validateName(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("이름은 빈 문자열이 될 수 없습니다.");
        }

        if (name.length() > MAX_NAME_LENGTH) {
            throw new IllegalArgumentException("이름은 " + MAX_NAME_LENGTH + "자 이하여야 합니다.");
        }
    }
}
  • 일단 Name이 있습니다. 해당 객체는 Value Object로 Car의 Name을 관리하는 것입니다.
  • 실제 여기서 5글자 이상이 될수 없다라는 요구사항을 충족시킬수 있습니다.
  • 아울러 해당 미션은 Java17로 진행하는 것이기 때문에 record를 써서 VO의 기본 기능들을 충족시켜주는 것이 좋을거라고 생각했습니다.
import racingcar.strategy.MoveStrategy;

public class Car {
    private static final int MIN_POSITION = 0;

    private final Name name;
    private int position;

    public Car(String name) {
        this(name, MIN_POSITION);
    }

    private Car(String name, int position) {
        this.name = new Name(name);
        this.position = position;
    }

    public void move(MoveStrategy moveStrategy) {
        if (moveStrategy.isMovable()) {
            position++;
        }
    }

    public int getPosition() {
        return position;
    }
}
  • 이제 이름과 해당 위치를 가지고 있는 Car 객체를 만듦니다.
  • 여기서 가장 중요한건 move입니다. 실제 move의 조건은 무작위한 값입니다. 무작위한 값이라는 것 자체가 실제 우리가 제어할 수 없는 값으로 볼수가 있습니다. 예를들어 테스트 코드에서 전진에 대한 테스트를 하고 싶지만. 무작위한 값이 나오기 때문에 테스트가 성공할수도 있고 실패할수도 있는 Flaky Test가 발생할 수 있습니다.
  • 이럴때 함수형 인터페이스를 사용해 함수를 1급 시민처럼 사용하는 함수형 프로그래밍을 사용해 인터페이스로 처리하는 방법으로 한다면 제어할 수 있습니다.
  • 필요시 마다 아래의 MoveStrategy의 구현체를 만들어 각각 구현해주면 됩니다.
@FunctionalInterface
public interface MoveStrategy {
    boolean isMovable();
}
import java.util.Random;

public class RandomMoveStrategy implements MoveStrategy {
    private static final Random RANDOM = new Random();
    private static final int MAX_MOVABLE_VALUE = 10;
    private static final int MIN_ADVANCE_VALUE = 4;

    @Override
    public boolean isMovable() {
        return RANDOM.nextInt(MAX_MOVABLE_VALUE) >= MIN_ADVANCE_VALUE;
    }
}
  • 프로덕션의 정책에 맞춰 간단하게 구현체를 만들어보았습니다.

TestCode

import static org.assertj.core.api.Assertions.assertThat;

class CarTest {
    @Nested
    class moveTest {
        private Car car;

        @BeforeEach
        void setUp() {
            car = new Car("marco");
        }

        @DisplayName("move 메서드에서 MoveStrategy가 true를 반환하면 position이 1 증가한다.")
        @Test
        void moveOfReturnTrueMoveStrategyTest() {
            car.move(() -> true);
            assertThat(car.getPosition()).isEqualTo(1);
        }

        @DisplayName("move 메서드에서 MoveStrategy가 false를 반환하면 position이 증가하지 않는다.")
        @Test
        void moveOfReturnFalseMoveStrategyTest() {
            car.move(() -> false);
            assertThat(car.getPosition()).isZero();
        }
    }
}

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

class NameTest {

    @DisplayName("생성자 테스트")
    @Nested
    class constructorTest {
        @DisplayName("이름이 5자 이하인 경우에 생성된다.")
        @Test
        void normalConstructorTest() {
            assertDoesNotThrow(() -> new Name("12345"));
        }

        @DisplayName("이름이 5자 이상인 경우에 에러가 발생한다.")
        @Test
        void validateNameOfMoreThanFiveLengthExceptionTest() {
            assertThatThrownBy(() -> new Name("123456"))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessage("이름은 5자 이하여야 합니다.");
        }

        @DisplayName("이름이 null이거나 빈값이면 에러가 발생한다.")
        @ParameterizedTest
        @NullAndEmptySource
        @ValueSource(strings = {" "})
        void validateNameOfNullAndEmptyExceptionTest(String value) {
            assertThatThrownBy(() -> new Name(value))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessage("이름은 빈 문자열이 될 수 없습니다.");
        }
    }
}
  • 테스트 코드는 위의 요구사항에 맞춰 테스트 시나리오를 잡아봣습니다.
  • 또한 Name이라는 ValueObject를 만들었기 때문에 실제 Name과 관련된 부분들은 Car보단 Name에 넣어서 단위 테스트를 진행하였습니다.
  • 하나의 메서드가 여러개의 테스트 시나리오가 나올수가 있기 때문에 조금더 명확히 보기 위해서 Nested로 테스트 코드를 작성해보았습니다.
    • Nested의 사용법은 아래 글을 참고해보세요.
    Junit5의 @Nested를 이용해 테스트 작성해보자

결론

  • 1주차 0단계 미션은 너무 많이 해본 것이였어서 빠르게 한번 진행햇던거 같습니다.
  • 아주 간단하게 Junit과 Java17을 경험해볼 수 있는 시간이였습니다.
728x90
728x90