* 본 아티클은, '좋은 코드 나쁜 코드' 10장을 기반으로 작성된 글입니다.
단위 테스트의 정의가 명확하지는 않지만, 상대적으로 격리된 방식의 클래스, 함수, 코드 파일을 테스트하는 것을 의미한다.
중요한 것은, 코드를 잘 테스트하고 이 작업을 유지보수할 수 있는 방법으로 수행하는 것이다.
몇 가지 중요한 용어를 짚고 넘어가자.
- 테스트 중인 코드 : 실제 코드를 의미한다. 테스트의 대상이 된다.
- 테스트 코드 : 테스트 중인 코드를 테스트하는 코드를 나타낸다. 단위 테스트를 구성한다.
- 테스트 케이스 : 테스트 코드의 각 파일에는 여러 테스트 케이스가 있을 수 있다. 하나의 동작, 시나리오를 테스트한다.
- 준비, 실행, 단언의 단계로 나뉘며, Given, When, Then으로 일컬어지기도 한다.
- 테스트 러너 : 테스트를 실제로 실행하는 도구다.
좋은 단위 테스트를 작성하는 법
- 훼손의 정확한 감지 : 코드가 훼손되면 테스트가 실패해야 하고, 테스트는 코드가 실제로 훼손된 경우에만 실패해야 한다.
- 세부 구현 사항에 독립적 : 리팩터링을 해도 테스트의 성공은 유지되어야 한다.
- 잘 설명되는 실패 : 실패 원인, 문제점을 명확하게 설명해야 한다.
- 이해할 수 있는 테스트 코드 : 다른 개발자들이 테스트 코드가 무엇을 테스트하고, 어떻게 수행되는지 쉽게 이해할 수 있어야 한다.
- 테스트 코드는 종종 실제 코드를 이해할 수 있는 문서로서의 역할도 담당한다.
- 쉽고 빠르게 실행 : 단위 테스트가 느리거나 실행이 어려우면 개발 시간이 낭비된다.
훼손의 정확한 감지
이를 통해 코드에 대한 초기 신뢰를 주고, 미래의 훼손을 막을 수 있다.
세부 구현 사항에 독립적
개발자가 코드베이스에 가할 수 있는 변경을 두 종류가 있다.
- 기능적 변화 - 외부에서 보이는 동작 수정 O
- 리팩터링 - 외부에서 보이는 동작 변경 X
기능적 변화가 일어난다면, 일반적으로 테스트도 수정해야 할 것으로 예상된다.
하지만, 리팩터링의 경우 테스트를 수정하지 않아도 되는 것이 바람직하다.
단위 테스트를 작성하는 두 가지 방식을 고려해보자.
A : 테스트는 코드의 모든 동작을 확인하고, 구현 세부사항을 확인한다. Private 멤버 변수 및 의존성을 직접 조작하여 상태를 시뮬레이션 한다.
B : 동작만 테스트할 뿐, 구현 세부 사항은 확인하지 않는다. 코드의 공개 API를 사용하여 상태를 설정하고, 가능한 곳에서 동작을 확인한다. Private 변수, 함수는 조작하거나 검증하지 않는다.
A 방식의 경우, 리팩터링을 올바르게 수행했더라도 테스트가 실패할 확률이 높다.
B 방식의 경우, 리팩터링을 올바르게 수행했다면 테스트가 여전히 성공할 것이다. 그렇지 않다면 리팩터링 과정이 잘못되지 않았는지 생각해볼 수 있다.
따라서 우리는, B 방식으로 동작만 테스트해야 한다.
세부 구현 사항을 테스트해서는 안 된다.
여기서 얻을 수 있는 또 하나의 인사이트는,
기능 변경과 리팩터링을 분리해야 한다는 것이다.
기능 변경을 동작을 변경하여 테스트 결과가 달라질 수 있기 때문이다.
잘 설명되는 실패
- 테스트 케이스가 한 가지 사항만 검사한다.
- 테스트 케이스에 대해 서술적인 이름을 사용한다.
- 테스트 실패 시 무엇이 잘못되었는지 명확하게 설명한다.
// 이해하기 어려운 실패
Test case testGetEvents failed: // 테스트 메서드명이 이해하기 어렵다.
Expected: [Event@ea4a92b, Event@3c5a99da] // 참조 변수의 주소값 -> 실패 이유를 알기 어렵다.
But was actually: [Event@3c5a99da, Event@ea4a92b]
// 이해하기 쉬운 실패
Test case testGetEvents_inChronologicalOrder failed: // 테스트 메서드명이 이해하기 쉽다.
Expected: [<Spaceflight, April 12, 1961>, <Moon Landing, July 20, 1969>]
But was actually: [<Moon Landing, July 20, 1969>, <Spaceflight, April 12, 1961>] // 순서가 잘못되었음을 명확하게 알 수 있다.
이해 가능한 테스트 코드
한 번에 너무 많은 것을 테스트하거나, 너무 많은 공유 테스트 설정을 하면 이해하기 어렵다.
테스트가 어렵다는 것은, 수정이 어려어진다는 것이다.
수정의 결과가 안전한지 이해하기 어렵기 때문이다.
일부 개발자들은, 테스트를 코드에 대한 사용 설명서로 사용한다.
코드의 사용법이나, 코드가 어떤 기능을 제공하는지 단위 테스트로 알아보는 것도 좋은 방법이다.
그렇기 때문에, 테스트 코드는 더욱이 이해하기 쉽게 작성되어야 한다.
쉽고 빠른 실행
테스트가 쉽고 빠르게 실행되어야 개발자들이 실제로 테스트할 수 있는 기회를 극대화할 수 있다.
퍼블릭 API에 집중하되, 중요한 동작은 무시하지 말라
일반적으로는 퍼블릭 API만을 사용하여 테스트해야 하고,
구현 세부 사항에 의존하면 안 된다.
하지만 테스트 대상 코드가 다른 코드에 의존할 수 있다. (굉장히 많은 경우 그렇다.)
의존하는 코드로부터 외부 입력이 제공되거나, 테스트 대상 코드가 의존하는 코드에 부수효과를 일으킨다면, 테스트의 의미가 달라질 수 있다.
아래의 경우를 생각해보자.
- 서버와 상호작용하는 코드
- 서버로부터 필요한 값을 받을 수 있도록 서버를 설정하거나 시뮬레이션해야 한다.
- 서버를 얼마나 자주 호출하는지, 요청이 유효한지 등 서버에 가하는 부수효과를 확인해야 할 수도 있다.
- 데이터베이스에 값을 저장하거나 읽는 코드
- DB에 저장된 여러 값으로 코드를 테스트하거나, 부수 효과로 DB에 어떤 값을 저장하는지 확인해야 할 수도 있다.
이런 경우, 퍼블릭 API만 테스트해서는 본질적인 것을 테스트할 수 없다.
퍼블릭 API만 사용하고, 구현 세부 사항을 의존하지 말라는 것은 훌륭한 조언이지만,
궁극적으로 중요한 것은 코드의 모든 중요한 동작을 제대로 테스트하는 것이다.
최대한 퍼블릭 API만으로 테스트하고, 대안이 없는 경우 퍼블릭 API를 벗어나 테스트해야 한다.
'프로그래밍 책' 카테고리의 다른 글
단위 테스트를 잘 작성하는 방법 #좋은코드나쁜코드 #11장 (0) | 2023.04.06 |
---|---|
테스트 더블이란? (목 Mock, 스텁 Stub, 페이크 Fake), 테스트 철학 #좋은코드나쁜코드 #10장 (0) | 2023.04.06 |
재사용, 일반화가 쉬운 코드 #좋은코드나쁜코드 #9장 (0) | 2023.04.05 |
모듈화된 코드 #좋은코드나쁜코드 #8장 (0) | 2023.04.05 |
오용하기 어려운 코드(불변, 빌더 패턴, 복사 패턴,) #좋은코드나쁜코드 #7장 (0) | 2023.04.05 |