java
테스트 코드의 구성 요소 : 상황, 실행, 결과 확인
- 테스트 코드는 기능을 실행하고 그 결과를 확인하므로 상황, 실행, 결과 확인의 세 가지 요소로 테스트를 구성할 수 있다.
- 어떤 상황이 주어지고(given), 그 상황에서 기능을 실행하고(when), 실행한 결과를 확인하는(then) 세 가지가 테스트 코드의 기본 골격을 이루게 된다.
- 기능은 상황에 따라 결과가 달라진다. 상황에 따라 다르게 동작하는 예시(숫자 야구 게임)를 살펴보자.
- 숫자 야구 게임은 0~9 사이의 서로 다른 숫자 세 개를 고르면 상대방이 그 숫자를 맞추면 게임이다. 즉, 이 게임에서의 상황은 정답 숫자이다.
@Test
void exactMatch() {
// 정답이 456인 상황 (= given)
BaseballGame game = new BaseballGame("456");
// 실행 (= when)
Score score = game.guess("456");
// 결과 확인 (= then)
assertThat(score.strike()).isEqaulTo(3);
}
@Test
void noMatch() {
// 정답이 123인 상황 (= given)
BaseballGame game = new BaseballGame("123");
// 실행 (= when)
Score score = game.guess("456");
// 결과 확인 (= then)
assertThat(score.strike()).isEqaulTo(0);
}
- 추가로, 여러 테스트에서 같은 상황이 있으면 @BeforeEach를 적용한 메서드에서 상황을 설정하는 것이 좋다.
- 상황이 없는 경우도 존재한다. TDD 흐름과 예시에서 본 예제가 이에 해당한다.
- 암호 강도 측정의 경우 결과에 영향을 주는 상황이 존재하지 않으므로 테스트는 다음처럼 기능을 실행하고 결과를 확인하는 코드만 포함하고 있다.
@Test
void meetsAllCriteria_Then_Strong() {
// 실행 (= when)
PasswordStrengthMeter meter = new PasswordStrengthMeter();
PasswordStrength result = meter.meter("ab12!@AB");
// 결과 확인 (= then)
assertThat(result).isEqualTo(PasswordStrength.STRONG);
}
- 또한, 실행 결과가 항상 리턴 값으로 존재하는 것은 아니다. 실행 결과로 예외를 발생하는 것이 정상인 경우도 있다.
@Test
void game_With_DupNumber_Then_Fail() {
assertThatThrownBy(() -> new BaseballGame("110"))
.isInstanceOf(IllegalArgumentException.class);
}
- 상황(given)-실행(when)-결과 확인(then) 구조에 너무 집착하지는 말자.
- 이 구조가 테스트 코드를 작성하는데 도움이 되는 것은 맞지만 꼭 모든 테스트 메서드를 이 구조로 만들어야 하는 것은 아니다. 테스트 코드를 보고 테스트 내용을 이해할 수 있으면 된다.
상황(given)에 대한 나의 생각
- 상황을 좀 더 구체적으로 말하자면, 결과에 영향을 주는 상황이다. 아래의 예시를 한번 보자.
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@InjectMocks
private UserService userService;
@Mock
private UserRepository userRepository; // UserRepository의 대역으로 Mock 사용
@Test
@DisplayName("회원 조회")
void findUser() {
// given
Long userId = 1L;
User user = User.builder()
.userId(userId)
.name("joyuri")
.build();
BDDMockito.given(userRepository.findByUserId(userId))
.willReturn(user); // Mock 객체가 Stub의 역할을 대신함
// when
User findUser = userService.findUser(userId);
// then
assertThat(findUser.getUserId()).isEqualTo(user.getUserId());
assertThat(findUser.getName()).isEqualTo(user.getName());
}
}
BDDMockito.given()
는 userService.findUser()
기능에 영향을 주고, 이는 곧 결과에 영향을 주므로 “결과에 영향을 주는 상황”의 의미가 잘 들어맞는다. 그런데 Long userId = 1L
과 User user = User.builder()...
부분은 어떨까? 이 부분은 “결과에 영향을 주는 상황”이라고 말하기에는 조금 애매해보인다.
- 위의 예시를 아래와 같이 하드 코딩하여 수정해보았다. 테스트는 문제 없이 잘 작동한다.
@Test
@DisplayName("회원 조회")
void findUser() {
// given
BDDMockito.given(userRepository.findByUserId(1L))
.willReturn(User.builder()
.userId(userId)
.name("joyuri")
.build(););
// when
User findUser = userService.findUser(1L);
// then
assertThat(findUser.getUserId()).isEqualTo(1L);
assertThat(findUser.getName()).isEqualTo("joyuri");
}
- 즉,
Long userId = 1L
과 User user = User.builder()...
부분은 “결과에 영향을 주는 상황”이라기보다는, when과 then에서의 “하드 코딩을 피하기 위해 변수를 선언하고 초기화한 것”의 의미가 더 적합해보인다.
- 그래서, 나의 결론은 다음과 같다.
- 테스트 코드도 코드이기 때문에, 하드 코딩보다는 변수를 활용하는 것이 좋다. 그러면 변수를 선언하고 초기화하는 부분은 어디에 작성할까? given 부분에 작성하지 말고 다른 부분에 작성하는 것보다는, 이 부분도 그냥 given 부분에 작성하는 것이 좋아보인다. given-when-then 구조를 유지할 수 있고, 일반적으로 많은 사람들도 given 부분에 작성하기 때문이다!
외부 상황과 외부 결과
- 상황에는 외부 요인도 있다.
- 상황에 영향을 주는 외부 요인은 파일, DBMS, 외부 서버 등 다양하다.
- 또한, 외부에서 결과를 확인해야 할 때도 있다.
- 예를 들어, 처리 결과를 지정한 경로의 파일에 저장하는 기능의 경우, 이 기능을 실행한 결과를 검증하려면 해당 경로에 파일이 원하는 내용으로 만들어졌는지 확인해야 한다.
외부 상황 예시 1 : 파일에서 데이터를 읽어오는 경우
@Test
void dataFileSumTest() {
// given
File dataFile = new File("src/test/resources/datafile.txt");
// when
long sum = MathUtils.sum(dataFile);
// then
assertThat(sum).isEqualTo(6L);
}
- 이 경우 다음 데이터를 갖는 “datafile.txt” 파일을 해당 경로에 미리 만들어야 하며, 다른 개발자도 테스트를 실행할 수 있어야 하므로 해당 파일은 버전 관리 대상에 추가해야 한다.
외부 상황 예시 2 : 계좌 번호 검증을 위해 외부 API를 사용하는 경우
@Test
void isValidAccountTest() throws Exception {
// given
String apiUrl = "https://example.com/api";
AccountValidator validator = new AccountValidator(apiUrl);
// when
boolean isValid = validator.isValidAccount("123456-78-910");
// then
assertThat(isValid).isTrue();
}
- 위와 같이 REST API 응답 결과가 유효한(또는 유효하지 않은) 계좌 번호인 상황은 테스트해볼 수 있지만, 아래의 상황들은 테스트해볼 수 없다.
- REST API 서버에 연결할 수 없는 상황
- REST API 서버에서 응답을 5초 이내에 받지 못하는 상황
- 이처럼 테스트 대상의 상황과 결과에 외부 요인이 관여할 경우에, 대역을 사용하면 테스트 작성이 쉬워진다.
- 대역은 테스트 대상이 의존하는 대상의 실제 구현을 대신하는 구현인데, 이 대역을 통해서 외부 상황이나 결과를 대체할 수 있다.
- 참고 자료
테스트 주도 개발 시작하기(저자: 최범균)