상태를 유지하며 일련의 시나리오를 테스트하고 싶으시다면
Junit의 DynamicTest를 활용해 보시면 좋을 것 같습니다.
지금처럼 상태를 공유하는 단위테스트는 지양하시는 것이 좋습니다.
체스 미션을 하면서 받았던 피드백입니다.
상태를 공유한다는 의미는 무엇일까? 시나리오 테스트는 무엇이고, DynamicTest는 어떻게 할 수 있을까?
코드를 수정해가며 알아보겠습니다!
Bad
우선 문제의(?) 테스트 코드의 일부입니다.
게임의 상태 `GameStatus`를 전역변수로 공유하여 사용하고 있습니다.
GameStatus gameStatus; // 상태를 공유하는 부분
Board board = new InitialBoardFactory().generate();
@BeforeEach
void setUp() { // 상태 초기화 부분
gameStatus = new GameStatus(...);
}
@DisplayName("ready에서 start를 할 수 있다")
@Test
void readyToStart() {
gameStatus.action(List.of("start"), board); // 상태 공유
assertThat(gameStatus.isStarted()).isTrue();
}
@DisplayName("start에서 move를 할 수 있다")
@Test
void startToMove() {
gameStatus.action(List.of("start"), board); // 상태 공유
gameStatus.action(List.of("move", "a2", "a4"), board);
assertThat(gameStatus.isMoved()).isTrue();
}
상태 공유가 왜 문제가 되는 것일까요?
테스트 간에 의도하지 않은 종속성이 생길 수 있기 때문입니다.
Better
따라서, 아래 코드가 더 좋은 코드라고 할 수 있습니다.
전역변수로 사용되었던 `gameStatus`가 지역변수로 변경되었습니다.
중복을 제거하면 좋은 코드라고 생각했었는데, 꼭 그런 것만은 아닌 것 같습니다.
이제 테스트 내부에서 코드를 한 번에 살펴볼 수도 있겠네요.
@DisplayName("ready에서 start를 할 수 있다")
@Test
void readyToStart() {
// given
GameStatus gameStatus = new GameStatus();
Board board = new InitialBoardFactory().generate();
// when
gameStatus.action(List.of("start"), board);
// then
assertThat(gameStatus.isStarted()).isTrue();
}
@DisplayName("start에서 move를 할 수 있다")
@Test
void startToMove() {
// given
GameStatus gameStatus = new GameStatus();
Board board = new InitialBoardFactory().generate();
// when
gameStatus.action(List.of("start"), board);
gameStatus.action(List.of("move", "a2", "a4"), board);
// then
assertThat(gameStatus.isMoved()).isTrue();
}
그러나, 아직 찜찜한 부분이 있습니다.
테스트를 원하는 상태에 도달하기 위한 코드가 불필요하게 많아집니다.
GameStatus 클래스는 `ready`> `start`> `move`> `finish`일방향의 상태를 갖습니다.
`move`를 테스트하기 위해서는, `ready to start`, `start to move`와 같이 상태를 변경하는 로직이 필요합니다.
상태가 더 많아진다면?
테스트를 위한 프로덕션 코드가 생겨날 위기(?)가 찾아올 것 같네요.
이때, 시나리오 테스트를 사용할 수 있습니다.
위 코드는 일련의 시나리오를 갖습니다.
- ready에서 start를 할 수 있다
- start에서 move를 할 수 있다
- …
시나리오 테스트를 적용하여, 코드 복잡도를 제거해 보겠습니다.
much Better
테스트 조건을 위한 코드가 줄었네요.
중첩 구조를 사용하여 시나리오대로 테스트가 진행되고 있습니다.
테스트의 순서를 제어할 수 있으니, 상태를 공유하면서도 종속성 문제를 우려하지 않아도 됩니다.
@TestFactory
Collection<DynamicTest> 게임_상태_변경_시나리오() {
GameStatus gameStatus = new GameStatus();
Board board = new InitialBoardFactory().generate();
return Arrays.asList(
DynamicTest.dynamicTest("ready에서 start를 할 수 있다", () -> {
gameStatus.action(List.of("start"), board);
assertThat(gameStatus.isStarted()).isTrue();
}),
DynamicTest.dynamicTest("start에서 move를 할 수 있다", () -> {
gameStatus.action(List.of("move", "a2", "a4"), board);
assertThat(gameStatus.isMoved()).isTrue();
})
);
}
테스트 제목을 붙일 수 있어, 일련의 시나리오를 한눈에 볼 수 있습니다.
가독성 측면에서도, 동작 흐름을 살펴볼 수 있으니 의도를 잘 전달할 수 있겠네요!

시나리오 테스트를 위해 사용했던 DynamicTest에 대해서 간단히만 짚고 가겠습니다.
DynamicTest
JUnit의 `DynamicTest` 동적 테스트는 정적 테스트 `Static Test`와 대조됩니다.
`@Test`가 대표적인 정적 테스트입니다.
Dynamic 키워드에서 알 수 있듯, 동적 테스트는 `런타임 시점`에 테스트가 생성되고 실행됩니다.
즉, 프로그램이 실행되는 도중에 상태를 사용할 수 있습니다.
정적 테스트에서는 컴파일 시점의 데이터를 사용합니다. `@ValueSource(ints = {-2, 0, 3, 10, 200})`와 같이 데이터를 정의하여 사용하는 방식입니다.
동적 테스트를 사용한다면, 런타임 시점의 데이터로 테스트할 수 있습니다. DB를 사용 시에 데이터를 가져와 사용할 수 있겠네요!
단, 동적 테스트는 정적 테스트와 생명 주기가 다르다는 점을 알고 있어야 합니다.
JUnit의 @BeforeEach @AfterEach와 같은 생명 주기와 관련된 기능을 사용할 수 없습니다.
정리
- 상태를 공유하는 테스트는 지양한다
- 단, 상태를 공유하며 시나리오 테스트를 하고 싶다면 DynamicTest를 사용할 수 있다
참고
'Java' 카테고리의 다른 글
의존성 역전을 언제 쓰나요? (3) | 2024.03.25 |
---|---|
돈은 머니머니해도 BigDecimal를 사용하자 (22) | 2024.03.18 |
객체 생성 방식에 기준이 있나요? (28) | 2024.03.11 |
도메인에 Record를 써도 될까요? (7) | 2024.03.04 |
상태를 유지하며 일련의 시나리오를 테스트하고 싶으시다면
Junit의 DynamicTest를 활용해 보시면 좋을 것 같습니다.
지금처럼 상태를 공유하는 단위테스트는 지양하시는 것이 좋습니다.
체스 미션을 하면서 받았던 피드백입니다.
상태를 공유한다는 의미는 무엇일까? 시나리오 테스트는 무엇이고, DynamicTest는 어떻게 할 수 있을까?
코드를 수정해가며 알아보겠습니다!
Bad
우선 문제의(?) 테스트 코드의 일부입니다.
게임의 상태 `GameStatus`를 전역변수로 공유하여 사용하고 있습니다.
GameStatus gameStatus; // 상태를 공유하는 부분
Board board = new InitialBoardFactory().generate();
@BeforeEach
void setUp() { // 상태 초기화 부분
gameStatus = new GameStatus(...);
}
@DisplayName("ready에서 start를 할 수 있다")
@Test
void readyToStart() {
gameStatus.action(List.of("start"), board); // 상태 공유
assertThat(gameStatus.isStarted()).isTrue();
}
@DisplayName("start에서 move를 할 수 있다")
@Test
void startToMove() {
gameStatus.action(List.of("start"), board); // 상태 공유
gameStatus.action(List.of("move", "a2", "a4"), board);
assertThat(gameStatus.isMoved()).isTrue();
}
상태 공유가 왜 문제가 되는 것일까요?
테스트 간에 의도하지 않은 종속성이 생길 수 있기 때문입니다.
Better
따라서, 아래 코드가 더 좋은 코드라고 할 수 있습니다.
전역변수로 사용되었던 `gameStatus`가 지역변수로 변경되었습니다.
중복을 제거하면 좋은 코드라고 생각했었는데, 꼭 그런 것만은 아닌 것 같습니다.
이제 테스트 내부에서 코드를 한 번에 살펴볼 수도 있겠네요.
@DisplayName("ready에서 start를 할 수 있다")
@Test
void readyToStart() {
// given
GameStatus gameStatus = new GameStatus();
Board board = new InitialBoardFactory().generate();
// when
gameStatus.action(List.of("start"), board);
// then
assertThat(gameStatus.isStarted()).isTrue();
}
@DisplayName("start에서 move를 할 수 있다")
@Test
void startToMove() {
// given
GameStatus gameStatus = new GameStatus();
Board board = new InitialBoardFactory().generate();
// when
gameStatus.action(List.of("start"), board);
gameStatus.action(List.of("move", "a2", "a4"), board);
// then
assertThat(gameStatus.isMoved()).isTrue();
}
그러나, 아직 찜찜한 부분이 있습니다.
테스트를 원하는 상태에 도달하기 위한 코드가 불필요하게 많아집니다.
GameStatus 클래스는 `ready`> `start`> `move`> `finish`일방향의 상태를 갖습니다.
`move`를 테스트하기 위해서는, `ready to start`, `start to move`와 같이 상태를 변경하는 로직이 필요합니다.
상태가 더 많아진다면?
테스트를 위한 프로덕션 코드가 생겨날 위기(?)가 찾아올 것 같네요.
이때, 시나리오 테스트를 사용할 수 있습니다.
위 코드는 일련의 시나리오를 갖습니다.
- ready에서 start를 할 수 있다
- start에서 move를 할 수 있다
- …
시나리오 테스트를 적용하여, 코드 복잡도를 제거해 보겠습니다.
much Better
테스트 조건을 위한 코드가 줄었네요.
중첩 구조를 사용하여 시나리오대로 테스트가 진행되고 있습니다.
테스트의 순서를 제어할 수 있으니, 상태를 공유하면서도 종속성 문제를 우려하지 않아도 됩니다.
@TestFactory
Collection<DynamicTest> 게임_상태_변경_시나리오() {
GameStatus gameStatus = new GameStatus();
Board board = new InitialBoardFactory().generate();
return Arrays.asList(
DynamicTest.dynamicTest("ready에서 start를 할 수 있다", () -> {
gameStatus.action(List.of("start"), board);
assertThat(gameStatus.isStarted()).isTrue();
}),
DynamicTest.dynamicTest("start에서 move를 할 수 있다", () -> {
gameStatus.action(List.of("move", "a2", "a4"), board);
assertThat(gameStatus.isMoved()).isTrue();
})
);
}
테스트 제목을 붙일 수 있어, 일련의 시나리오를 한눈에 볼 수 있습니다.
가독성 측면에서도, 동작 흐름을 살펴볼 수 있으니 의도를 잘 전달할 수 있겠네요!

시나리오 테스트를 위해 사용했던 DynamicTest에 대해서 간단히만 짚고 가겠습니다.
DynamicTest
JUnit의 `DynamicTest` 동적 테스트는 정적 테스트 `Static Test`와 대조됩니다.
`@Test`가 대표적인 정적 테스트입니다.
Dynamic 키워드에서 알 수 있듯, 동적 테스트는 `런타임 시점`에 테스트가 생성되고 실행됩니다.
즉, 프로그램이 실행되는 도중에 상태를 사용할 수 있습니다.
정적 테스트에서는 컴파일 시점의 데이터를 사용합니다. `@ValueSource(ints = {-2, 0, 3, 10, 200})`와 같이 데이터를 정의하여 사용하는 방식입니다.
동적 테스트를 사용한다면, 런타임 시점의 데이터로 테스트할 수 있습니다. DB를 사용 시에 데이터를 가져와 사용할 수 있겠네요!
단, 동적 테스트는 정적 테스트와 생명 주기가 다르다는 점을 알고 있어야 합니다.
JUnit의 @BeforeEach @AfterEach와 같은 생명 주기와 관련된 기능을 사용할 수 없습니다.
정리
- 상태를 공유하는 테스트는 지양한다
- 단, 상태를 공유하며 시나리오 테스트를 하고 싶다면 DynamicTest를 사용할 수 있다
참고
'Java' 카테고리의 다른 글
의존성 역전을 언제 쓰나요? (3) | 2024.03.25 |
---|---|
돈은 머니머니해도 BigDecimal를 사용하자 (22) | 2024.03.18 |
객체 생성 방식에 기준이 있나요? (28) | 2024.03.11 |
도메인에 Record를 써도 될까요? (7) | 2024.03.04 |