suyeonme

[테스트] 좋은 단위 테스트(Unit Test) 작성하기 본문

프로그래밍👩🏻‍💻/기타

[테스트] 좋은 단위 테스트(Unit Test) 작성하기

suyeonme 2022. 12. 10. 12:40

단위 테스트(Unit Test)


단위 테스트는 단일 클래스나 메서드처럼 범위가 상대적으로 좁은 테스트를 뜻한다.

단위 테스트 장점

  • 단위 테스트는 빠르고 결정적(deterministic)이여서 개발자들이 수시로 수행하며 피드백을 즉각 얻을 수 있다.
  • 대상 코드와 동시에 작성할 수 있을만큼 작성하기 쉽다.
  • 빠르게 작성할 수 있으므로 테스트 커버리지를 높이기 좋다.
  • 테스트는 개념적으로 간단하고 시스템의 특정 부분에 집중하므로 실패시 원인 파악이 쉽다.
  • 대상 시스템의 사용법과 의도한 동작 방식을 알려주는 문서자료 역할을 한다.

단위 테스트를 수행해야하는 시점

  • 순수 리팩터링
  • 새로운 기능 추가
  • 버그 수정
  • 코드의 행위 변경

좋은 단위 테스트를 작성하는 원칙 


공개 API를 이용해 테스트하자

  • 내부 구현을 위한 코드가 아닌 공개 API를 호출해서 테스트를 한다.
  • 예를 들어, 똑같이 public으로 지정된 메서드라도 다른 공개 API를 구현하면서 생긴 파생 메서드일 수 있다. 이런 메서드는 내부구현에 해당하므로 직접 테스트하지 않고 다른 공개 API를 테스트하는 과정에서 간접적으로 테스트한다.

상호작용이 아니라 상태를 테스트하자

상태 테스트(state test)

  • 메소드 호출 후 시스템 자체를 관찰하는 테스트

상호작용 테스트(interaction test)

  • 호출을 처리하는 과정에서 시스템이 다른 시스템과 협력하여 기대한 일련의 동작을 수행하는지 확인하는 테스트
  • 무엇(what)이 아닌 어떻게(how) 작동하는지를 확인하기 때문에 상태 테스트보다 깨지기 쉽다.

행위 주도 테스트로 테스트를 완전하고 간결하게 만들자

  • 완전한 테스트(complete test): 결과에 도달하기까지의 논리를 읽는 이가 이해하는데 필요한 모든 정보를 본문에 담고있는 테스트
  • 간결한 테스트(concise test): 코드가 산만하지 않고 관련 없는 정보는 포함하지 않은 테스트

테스트를 메서드별로 작성하지 말고 행위(behavior)별로 작성해야한다. 즉 메서드 중심이 아닌 행위 주도 테스트를 작성한다.

// 메서드 중심 테스트
@Test
public void testDisplayTransactionResults() {
  transactionProcessor.displayTransactionResult(
    newUserWithBalance(LOW_BALANCE_THRESOLD.plus(dollars(2))),
    new Transaction("물품", dollars(3))
  );
  assertThat(ui.getText()).contains("물품 구입");
  assertThat(ui.getText()).contains("잔고 부족");
}

// 행위 중심 테스트
@Test
public void displayTransactionResults_showItemName() {
  transactionProcessor.displayTransactionResult(new User(), new Transaction("물품"));
  assertThat(ui.getText()).contains("물품 구입");
}

@Test
public void displayTransactionResults_showLowBalanceWarning() {
  transactionProcessor.displayTransactionResult(
    newUserWithBalance(LOW_BALANCE_THRESOLD.plus(dollars(2))),
    new Transaction("물품", dollars(3))
  );
  assertThat(ui.getText()).contains("잔고 부족");
}

행위 주도 테스트  행위를 때로는 given/when/then을 써서 표현하기도 한다. 이 문법을 지원하는 프레임워크로 Cucumber, Spock이 있다.  테스트 이름은 should를 사용해도 좋다.(e.g. shouldNotAllowWithdrawalsWhenBalanceIsEmpty)

  • given: 시스템의 설정을 정의
  • when: 시스템이 수행할 작업
  • then: 결과를 검증
  • and: 긴 블록을 쪼갠 후, and으로 연결한다.
@Test
public void transferFundsShouldMoveMoneyBetweenAccounts() {
  // Given: 두개의 계좌, 각각의 잔고는 $150, $20
  Account account1 = newAccountWithBalance(usd(150));
  Account account2 = newAccountWithBalance(usd(20));
  
  // When: 첫번째 계좌에서 두번째 계좌로 $100 이체
  bank.transferFunds(account1, account2, usd(100));
  
  // Then: 각 계좌 잔고에 이체 결과가 반영됨
  assertThat(account1.getBalance()).isEqualTo(usd(50));
  assertThat(account2.getBalance()).isEqualTo(usd(120));
}

테스트에는 논리를 넣지 말자

테스트 코드에는 스마트한 로직보다 직설적인 코드를 고집해야한다. 더 서술적이고 의미있는 테스트코드를 만들기 위한 약간의 정복은 허용해도 좋다. 테스트 코드에는 DRY가 아니라 DAMP(Descriptive And Meaningful Phrase)가 되도록 노력해야한다.

// 논리가 버그를 감추는 코드
@Test
public void shouldNavigateToAlbumPage() {
  String baseUrl = "http://phtos.google.com/";
  Navigator nav = new Navigator(baseUrl);
  nav.goToAlbumPage();
  assertThat(nav.getCurrentUrl()).isEqualTo(baseUrl + "/albums"); // BUG! 알아차리기 어려움
}

// 논리를 제거하니 버그가 드러남
@Test
public void shouldNavigateToAlbumPage() {
  Navigator nav = new Navigator("http://phtos.google.com/");
  nav.goToAlbumPage();
  assertThat(nav.getCurrentUrl()).isEqualTo("http://phtos.google.com//albums"); // BUG!
}
Comments