좋은 코드, 나쁜 코드 북스터디 - chap10. 단위 테스트의 원칙

10. 단위 테스트의 원칙

단위 테스트는 상대적으로 격리된 방식으로 코드의 구별되는 단위를 테스트하는 것에 관한 것이다. 코드의 단위(unit of code)라는 것이 정확히 의미하는 바는 다양할 수 있지만, 특정 클래스, 함수, 코드 파일을 의미할 때가 많다.

10.1. 단위 테스트 기초

  • 테스트 중인 코드: 실제 코드
  • 테스트 코드: 실제 코드를 사용하여 테스트를 수행하는 코드
  • 테스트 케이스: 테스트 코드 내의 특정 동작이나 시나리오를 테스트하는 코드 조각
    • 준비: BDD의 given
    • 실행: BDD의 when
    • 단언: BDD의 then

10.2. 좋은 단위 테스트는 어떻게 작성할 수 있는가?

좋은 단위 테스트가 가져야 할 5가지 중요 기능

  • 훼손의 정확한 감지
  • 세부 구현 사항에 독립적
  • 잘 설명되는 실패
  • 이해 가능한 테스트 코드
  • 쉽고 빠른 실행

10.2.1. 훼손의 정확한 감지

코드가 훼손되었을 때 실패하는 테스트 코드를 작성하는 것이 중요하다.

실제 코드가 정상임에도 불구하고 조건 또는 불규칙적으로 실패하는 테스트를 플래키(flakey) 라고 한다. 보통 무작위성, 타이밍 기반 레이스(timing-based race) 조건, 외부 시스템에 의존하는 등의 테스트의 비결정적(indeterministic) 동작에 기인한다.

10.2.2. 세부 구현 사항에 독립적

기능 변경과 리팩터링을 같이 하지 말라 기능적 변화와 리팩터링을 동시에 하면 기능적 변화로 예상되는 동작의 변화와 리팩터링의 실수로 발생하는 동작의 변화를 구분하기 어려울 수 있다. 보통 리팩터링을 한 다음 기능 변경을 따로 하는 것이 좋다.

세부 구현 사항에 종속적 테스트를 만들경우, 리팩토링 시 테스트까지 같이 변경해야 하고, 그럼 리팩토링 전후의 동작이 동일하다는 것을 보장하기 힘들다.

10.2.3. 잘 설명되는 실패

테스트가 실패했을 때, 어디에서 어떻게 실패했는지 잘 나타내는것도 중요하다.

10.2.4. 이해 가능한 테스트 코드

테스트 코드도 유지보수의 대상이고, 어떤 개발자에겐 실제 코드에 대한 사용 설명서로 인식되기 때문에 가독성도 신경써야한다.

10.2.5. 쉽고 빠른 실행

테스트 실행이 쉽지 않거나 느리게 된다면 자주 실행시키지 않게되고, 심할경우 해당 테스트를 무시하고 다른 테스트만 실행하는 경우도 생긴다. 그럼 테스트의 의미가 퇴색된다.

10.3. 퍼블릭 API에 집중하되 중요한 동작은 무시하지 말라

일반적으로는 ‘퍼블릭 API만을 사용한 테스트’가 원칙이지만, 실제 코드의 외부 의존성이 있을경우 외부 의존성에 대한 내용도 확인이 필요하다. 이럴경우 목이나 페이크를 통해 외부 의존성과의 커뮤니케이션이 의도대로 수행되는지 확인하는 테스트 코드가 필요하다.

10.4. 테스트 더블

테스트 더블은 의존성을 시뮬레이션하는 객체지만 테스트에 더 적합하게 사용할 수 있도록 만들어진다.

  • 스텁
  • 페이크

10.4.1. 테스트 더블을 사용하는 이유

  • 테스트 단순화
    • 실제 의존성 체인을 만들어 주입하지 않아도 됨
  • 테스트로부터 외부 세계 보호
    • 테스트로인해 실제 외부 세계의 데이터 변경을 보호
  • 외부로부터 테스트 보호
    • 비결정적 외부 환경으로부터 테스트 보호

10.4.2. 목

목(mock)은 클래스나 인터페이스를 시뮬레이션하는 데 멤버 함수에 대한 호출을 기록하는 것 외에는 어떠한 일도 수행하지 않는다.

사실 여기에서 이야기하는 목은 스파이(spy)에 가깝다.

10.4.3. 스텁

스텁(stub)은 함수가 호출되면 미리 정해 놓은 값을 반환함으로써 함수를 시뮬레이션한다.

10.4.4. 목과 스텁은 문제가 될 수 있다

  • 목이나 스텁이 실제 의존성과 다른 방식으로 동작하도록 설정되면 테스트는 실제적이지 않다.
  • 구현 세부 사항과 테스트가 밀접하게 결합하여 리팩터링이 어려워질 수 있다.

10.4.5 페이크

페이크는 실제 의존성의 공개 API를 정확하게 시뮬레이션하지만 구현은 일반적으로 단순한데, 외부 시스템과 통신하는 대신 페이크 내의 멤버 변수에 상태를 저장한다.

실제 의존성에 대한 코드를 유지보수하는 팀이 일반적으로 페이크 코드도 유지보수해야 하는데, 실제 의존성에 대한 코드 계약이 변경되면 페이크의 코드 계약도 동일하게 변경되어야 하기 때문이다.

10.4.6 목에 대한 의견

필자는 페이크를 사용하는 쪽이다. 나도 페이크에 한표.

목이나 스텁을 사용하는 것도 나쁘다고 생각하는 것은 아니나, 잘 쓰려면 실제 코드를 잘 설계해야한다 생각하는데, 언제나 잘 설계하는 것은 쉬운일이 아니다.

10.5. 테스트 철학으로부터 신중하게 선택하라

  • TDD
  • BDD
  • ATDD
Hugo로 만듦
JimmyStack 테마 사용 중