Study/TDD

TDD - 테스트 코드의 구성

hou27 2023. 2. 3. 19:57

이전 포스트

https://hou27.tistory.com/entry/TDD-JUnit-5-%EA%B8%B0%EC%B4%88

 

TDD - JUnit 5 기초

이전 포스트 https://hou27.tistory.com/entry/TDD-TDD-%E2%88%99-%EA%B8%B0%EB%8A%A5-%EB%AA%85%EC%84%B8-%E2%88%99-%EC%84%A4%EA%B3%84 TDD - TDD ∙ 기능 명세 ∙ 설계 이전 포스트 https://hou27.tistory.com/entry/TDD-%ED%85%8C%EC%8A%A4%ED%8A%B8-%E

hou27.tistory.com

 

실습 환경

- Java 17

- Spring Boot 3.0.1

- wsl 2

 

기능에서의 상황

기능은 상황에 따라 다르게 동작한다.

그렇기 때문에 결과에 영향을 주는 상황을 찾고 코드에 반영해야

예상치 못한 버그를 방지할 수 있다.

 

 

테스트 코드의 구성 요소 : 상황, 실행, 결과

위에서 언급한 '기능은 상황에 따라 다르게 동작한다'는

곧 상황에 따라 결과가 달라진다는 뜻이다.

 

테스트 코드는 기능을 실행하고 결과를 확인하는데,

우리는 여기에 상황을 부여할 수 있다.

 

상황, 실행, 결과 확인은 영어로 given, when, then이라고 표현한다.

 

이전에 구현했던 암호 강도 측정기의 테스트 코드를 살펴보면

@Test
void meetsAllCriteria_Then_Strong() {
  // 실행
  PasswordStrengthMeter meter = new PasswordStrengthMeter();
  PasswordStrength result = meter.meter("123ABCDE456");
  
  // 결과 확인
  assertEquals(PasswordStrength.STRONG, result);
}

결과에 영향을 주는 상황이 존재하지 않아서

실행하고 결과를 확인하는 코드만 존재하는 것을 확인할 수 있다.

 

그렇다면 상황이 있는 예시를 살펴보자

@Test
void exactMatch() {
  // given
  BaseballGame game = new BaseballGame("456");
  
  // when
  Score score = game.guess("456");
  
  // then
  assertEqual(3, score.strikes());
  assertEqual(0, score.balls());
}

위 코드는 숫자 야구 게임을 테스트하는 코드이다.

정답이 456인 상황이 주어졌고,

그 상황에서 실행과 결과 확인을 진행하고 있다.

 

그러나 실행 결과가 항상 리턴 값으로 존재하진 않는다.

@Test
void baseballGameException() {
  assertThrows(IllegalArgumentException.class, () -> 
    new BaseballGame("110")
  );
}

숫자 야구 게임 생성 기능은 동일한 숫자가 사용될 수 없도록 설계되었다고 가정해본다면,

위 상황에선 실행 시 IllegalArgumentException를 발생시키도록 구현할 수 있다.

 

외부 상황과 외부 결과

테스트 대상이 아닌 외부 요인이 상황에 영향을 미치는 경우도 있다.

@Test
void noDataFile_Then_Exception() {
  File dataFile = new File("badpath.txt");
  assertThrows(IllegalArgumentException.class, () -> {
    new DataScanner(dataFile);
  });
}

위와 같은 경우는

존재하지 않는 파일을 임의의 클래스인 DataScanner에 넘겨주게 되면,

에러를 던지도록 작성한 코드이다.

 

그러나 여기엔 문제가 있다.

 

테스트는 항상 실행할 때마다 동일한 결과를 보장해야하는데,

외부 파일이 상황에 영향을 주고 있기 때문에

 

우연히 badpath.txt라는 파일이 존재하는 경우, 테스트가 의도한 결과를 이끌어낼 수 없게 된다.

 

외부 상태가 테스트 결과에 영향을 주지 않게 하기

private void givenNoFile(String path) {
  File file = new File(path);
  if(file.exists()) {
    file.delete();
  }
}

@Test
void noDataFile_Then_Exception() {
  givenNoFile("badpath.txt");

  File dataFile = new File("badpath.txt");
  assertThrows(IllegalArgumentException.class, () -> {
    new DataScanner(dataFile);
  });
}

그래서 위와 같이

외부 상황에 따라 테스트 결과가 변하지 않도록

확실하게 파일이 없는 상황을 만들어 테스트를 신뢰할 수 있도록 해주어야 한다.

 

이번에 살펴볼 경우는 DB가 테스트에 사용되는 경우이다.

보통 테스트 코드는 한 번만 실행하고 끝나지 않는다.

TDD를 진행하며 계속해서 실행하고, 개발이 끝난 후에도 테스트를 통해 검증한다.

 

즉, 테스트는 항상 정상적으로 동작하는 것이 매우 중요하다.

 

회원가입 기능을 예로 살펴보겠다.

회원가입 시, 가입하려는 ID가 이미 DB 테이블에 존재한다면 가입 실패하도록

로직이 짜여져 있다고 가정해보자

 

@Test
void registerSuccessfully() {
  // given
  RegistReq req = new RegistReq("testID", "passw0rd", "김정호");

  // when
  registerService.register(req);

  // then
  User user = userRepository.findById(req.getId());
  assertEquals("김정호", user.getName());
}

다음과 같이 테스트 코드를 작성했을 때,

첫번째 실행에선 testID로 회원가입이 성공적으로 진행되어

테스트가 성공적으로 통과할 것이다.

 

그러나 다시 테스트를 실행한다면 DB에 이미 testID로 계정이 존재하기 때문에

중복으로 테스트에 실패하게 될 것이다.

 

이렇게 외부 요인으로 인해 테스트의 성공 여부가 바뀌지 않으려면,

- 테스트 전 외부를 원하는 상태로 만들거나,

- 테스트 후 외부를 원래대로 복구해야 한다.

 

외부 상태와 테스트 어려움

테스트 대상이 아닌 외부 요인은 코드에서 다루기 매우 힘든 영역이다.

테스트에 영향을 미치는데, 마음대로 제어할 수 없는 경우도 있기 때문에

다음 챕터에서 살펴볼 '대역'을 사용하여 난이도를 낮춘다고 한다.

'Study > TDD' 카테고리의 다른 글

TDD - 테스트가 가능한 설계  (0) 2023.02.14
TDD - 대역  (0) 2023.02.08
TDD - JUnit 5 기초  (0) 2023.01.25
TDD - TDD ∙ 기능 명세 ∙ 설계  (2) 2023.01.16
TDD - 테스트 코드 작성 순서  (0) 2023.01.12