좋은 코드, 나쁜 코드 도서를 요약한 내용입니다.
10장. 단위 테스트의 원칙
코드가 처음 작성될 때 그리고 수정될 때마다 코드가 의도한 대로 작동한다는 것을 스스로 확신할 수 있는 방법이 필요하다. 테스트는 이러한 확신을 하기 위한 주된 방법이다.
10.1 단위 테스트 기초
- 테스트 중인 코드(code under test):
'실제 코드'
라고도 하고 테스트의 대상이 되는 코드 - 테스트 코드(test code): 단위 테스트를 구성하는 코드
- 테스트 케이스(test case): 특정 동작이나 시나리오를 테스트
- 준비(arrange): 테스트 값을 정의, 의존성 설정, 인스턴스 설정
- 실행(act): 동작을 실제로 호출하는 코드
- 단언(assert): 반환값이 예상한 값과 같은가
- 테스트 러너(test runner): 테스트를 실행하는 도구
10.2 좋은 단위 테스트는 어떻게 작성할 수 있는가?
좋은 단위 테스트가 가져야 할 5가지 주요 기능
- 훼손의 정확한 감지: 코드가 훼손되면 테스트가 실패한다.
- 세부 구현 사항에 독립적: 세부 구현 사항을 변경하더라도 테스트 코드는 변경하지 않는 것이 이상적이다.
- 잘 설명되는 실패: 코드가 잘못되면 테스트는 실패의 원인과 문제점을 명확하게 설명해야 한다.
- 이해할 수 있는 테스트 코드: 다른 개발자들이 테스트 코드가 정확히 무엇을 테스트하기 위한 것이고 테스트가 어떻게 수행되는지 이해할 수 있어야 한다.
- 쉽고 빠르게 실행: 단위 테스트가 느리거나 실행이 어려우면 개발 시간이 낭비된다.
10.2.1 훼손의 정확한 감지
단위 테스트의 가장 명확하고 주된 목표는 코드가 훼손되지 않았는지 확인하는 것이다.
- 코드에 대한 초기 신뢰를 준다. 아무리 신중하게 코드를 작성해도 실수는 있기 마련이다. 새로운 코드나 코드 변경 사항과 함께 철저한 테스트 코드를 작성하면 코드가 코드베이스로 병합되기 전에 이러한 실수를 발견하고 수정할 수 있다.
- 미래의 훼손을 막아준다. 어느 시점에 다른 개발자가 코드를 변경하는 과정에서 실수로 코드를 훼손할 가능성이 크다. 이것에 대한 유일한 효과적인 방어 방법은 코드가 컴파일을 중지하거나 테스트가 실패하는 것이다. 코드 변경으로 인해 잘 돌아가던 기능이 작동하지 않는 것을 회귀(regression)라고 한다. 이러한 회귀를 탐지할 목적으로 테스트를 실행하는 것을 회귀 테스트라고 한다.
10.2.2 세부 구현 사항에 독립적
일반적으로 개발자가 코드베이스에 가할 수 있는 변경은 두 가지 종류가 있다.
- 기능적 변환: 새로운 기능 추가, 버그 수정, 에러 처리
- 리팩터링: 큰 함수를 작은 함수로 분활하거나 재사용하기 쉽도록 일부 유틸리티 코드를 다른 파일로 옮기는 등의 코드의 구조적 변화를 의미한다.
기능 변경과 리팩터링을 같이 하지 말라
10.2.3 잘 설명되는 실패
실패에 대한 자세한 내용을 보여주지 않는 테스트 실패
Test case testGetEvents failed // 테스트 케이스의 이름으로부터 어떤 동작을 테스트하는지 알 수 없다.
Expected: [Event@ea4a92b, Event@3c5a99da]
But was actually: [Event@3c5a99da, Event@ea4a92b] // 실패 메시지가 이해하기 어렵다.
실패에 대한 자세한 내용을 잘 설명해주는 테스트 실패
Test case testGetEvents_inChronologicalOrder failed // 테스트 케이스의 이름으로부터 어떤 동작이 테스트되고 있는지 알 수 있다.
Contents match, but order differs
Expected:
[<Spaceflight, April 12, 1961>, <Moon Landing, July 20, 1969>]
But was actually:
[<Moon Landing, July 20, 1969>, <Spaceflight, April 12, 1961>] // 실패 메시지가 명확하다.
10.2.4 이해 가능한 테스트 코드
새로운 요구사항을 충족하기 위해 코드의 기능을 의도적으로 수정할 수 있다. 또한 테스트 코드도 수정해야 한다.
개발자가 자신이 변경한 사항이 원하는 동작에만 영향을 미친다는 확신을 가지려면 테스트의 어느 부분에 영향을 미치고 있는지, 테스트 코드에 대한 수정이 필요한지 여부를 알 수 있어야 한다.
10.2.5 쉽고 빠른 실행
단위 테스트를 실행하는 데 한 시간이 걸린다면 코드 변경 병합 요청이 작거나 사소한 것과 상관없이 최소한 한 시간이 걸리기 때문에 모든 개발자의 속도가 느려진다.
테스트를 빠르고 쉽게 유지해야 하는 또 다른 이유는 개발자가 실제로 테스트를 할 수 있는 기회를 극대화하기 위함이다.
10.3 퍼블릭 API에 집중하되 중요한 동작은 무시하지 말라
10.3.1 중요한 동작이 퍼블릭 API 외부에 있을 수 있다
10.4 테스트 더블
세 가지 유형의 테스트 더블, 즉 목(mock), 스텁(stub), 페이크(fake)에 대해 살펴본다.
10.4.1 테스트 더블을 사용하는 이유
- 테스트 단순화: 의존성을 실제로 사용하는 대신 테스트 더블을 사용하면 작업이 단순해진다.
- 테스트로부터 외부 세계 보호: 일부 의존성은 실제로 부수 효과를 발생한다. 코드의 종속성 중 하나가 실제 서버에 요청을 전송하거나 실제 데이터베이스에 값을 쓰게 되면, 사용자나 비즈니스에 중요한 프로세스에 나쁜 결과를 초래할 수 있다.
- 외부로부터 테스트 보호: 다른 시스템이 데이터베이스에 쓴 값을 의존성 코드가 읽는다면 이 값은 시간이 지남에 따라 변경될 수 있다.
10.4.2 목
목(mock)은 클래스나 인터페이스를 시뮬레이션하는 데 멤버 함수에 대한 호출을 기록하는 것 외에는 어떠한 일도 수행하지 않는다.
10.4.3 스텁
스텁(stub)은 함수가 호출되면 미리 정해 놓은 값을 반환함으로써 함수를 시뮬레이션 한다.