2025. 4. 9. 20:53ㆍ테스트 코드
2025.03.31 - [테스트 코드] - 단위 테스트 (1) - 테스트를 바라보는 관점
단위 테스트 (1) - 테스트를 바라보는 관점
테스트에는 단위 테스트, 통합 테스트, E2E 테스트가 있다. 단위 테스트는 가장 작은 단위의 테스트로 하나의 메서드나 클래스를 독립적으로 테스트한다. 통합 테스트는 여러 모듈이나 컴포넌트
iseongmin.tistory.com
2025.04.09 - [테스트 코드] - 단위 테스트 (2) - 가치 있는 테스트
단위 테스트 (2) - 가치 있는 테스트
테스트에는 단위 테스트, 통합 테스트, E2E 테스트가 있다. 단위 테스트는 가장 작은 단위의 테스트로 하나의 메서드나 클래스를 독립적으로 테스트한다. 통합 테스트는 여러 모듈이나 컴포넌트
iseongmin.tistory.com
2025.05.05 - [테스트 코드] - 단위 테스트 (3) - 테스트 코드 작성 스타일
단위 테스트 (3) - 테스트 코드 작성 스타일
테스트에는 단위 테스트, 통합 테스트, E2E 테스트가 있다. 단위 테스트는 가장 작은 단위의 테스트로 하나의 메서드나 클래스를 독립적으로 테스트한다. 통합 테스트는 여러 모듈이나 컴포넌트
iseongmin.tistory.com
테스트에는 단위 테스트, 통합 테스트, E2E 테스트가 있다. 단위 테스트는 가장 작은 단위의 테스트로 하나의 메서드나 클래스를 독립적으로 테스트한다. 통합 테스트는 여러 모듈이나 컴포넌트가 함께 동작할 때 올바르게 연결되는지 테스트한다. E2E 테스트는 실제 사용자가 애플리케이션을 이용하는 흐름을 시뮬레이션하여, 전체 시스템이 의도대로 작동하는지 테스트한다.
나는 실무를 진행하면서, 다른 테스트는 아니더라도 단위 테스트만큼은 꼭 작성하려고 노력하고 있다. 물론, 모든 코드에 테스트를 작성하는 것은 아니고, 비즈니스에 핵심적인 도메인 로직 위주로 테스트를 작성한다. 단위 테스트는 다른 종류의 테스트를 작성하기 위한 기초이기 때문에, 통합 테스트와 E2E 테스트로 나아가기 위한 첫 걸음이라고 생각한다.
사실 이 책을 읽기 전에도 단위 테스트를 작성하고 있었지만, 어느 순간부터 내가 작성하는 코드가 단위 테스트인지 통합 테스트인지 경계가 흐려지기 시작했다. 목업(mock)이 점점 과해졌고, 지나친 세부 구현에 대한 검증을 하다 보니 테스트 코드가 유리처럼 깨지기 운 상태가 되어버렸다. 작은 리팩터링만 해도 테스트가 우수수 깨지기 시작했고, 테스트가 오히려 개발의 발복을 잡는 상황이 생기곤 했다.
그런 경험이 반복되면서 테스트 코드 작성 자체가 점점 부담으로 느껴졌고, 어느새 "원래 테스트 코드 작성은 이런건가?"라는 의문이 들기 시작했다. 이 의문을 풀어보고 싶다는 생각에, 이 책을 읽게 되었다.
가치 있는 테스트를 작성하려면 가치 있는 테스트를 식별할 수 있어야 한다.
좋은 단위 테스트에는 다음 네 가지 특성이 있다.
- 회귀 방지
- 리팩토링 내성
- 빠른 피드백
- 유지 보수성
1. 회귀 방지
회귀는 코드 수정 후 기존 기능이 의도한 대로 동작하는 않는 버그를 말한다. 이를 방지하려면 테스트가 최대한 많은 코드를 실행하도록 하고, 비즈니스 로직과 같이 핵심적인 영역을 집중적으로 테스트해야 한다.
2. 리팩토링 내성
리팩토링 내성은 애플리케이션 코드를 리팩토링 했을 때, 기존 테스트 코드가 실패하지 않고 그대로 통과하는지의 척도다. 리팩토링 내성이 높을수록 코드 변경에 강한 테스트라는 의미다. 이를 높이기 위해서는 거짓 양성을 줄이는 것이 중요하다. 거짓 양성이란, 실제로 기능은 정상적으로 작동하고 있음에도 테스트가 실패하는 경우를 말한다.
거짓 양성은 테스트가 타당한 이유 없이 실패하는 현상이기 때문에, 개발자들이 점점 테스트 결과를 신뢰하지 않게 만든다. 그렇게 되면 정말로 타당한 실패조차도 놓치기 쉬워지고, 테스트에 대한 신뢰도는 빠르게 떨어진다. 결국 테스트는 안전망으로서의 역할을 잃게 되고, 자연스럽게 리팩토링도 줄어들게 된다.
처음에는 개발자들이 테스트가 실패하면 액면 그대로 받아들이고 이에 따라 적절히 처리한다. 얼마 후, 사람들은 항상 '늑대'라고 외치는 테스트에 질려서 점점 무시하기 시작한다. 결국 개발자가 모든 거짓 양성과 함께 실패를 무시했기 때문에 실제 버그가 운영 환경에 릴리즈 되는 순간이 온다.
거짓 양성이 발생하는 원인은 테스트와 테스트 대상 시스템(SUT)의 구현 세부 사항이 너무 많이 결합되어 있기 때문이다. 이러한 테스트는 리팩토링 내성이 없으며, 코드가 조금만 변경되어도 쉽게 실패한다. 이를 방지하려면 테스트와 SUT가 최대한 느슨하게 결합되어야 한다. 즉, 구현 세부 사항 대신 최종 결과만을 검증해야 한다.
3. 빠른 피드백
빠른 피드백은 단위 테스트의 필수 속성이다. 테스트 속도가 빠를수록 테스트 스위트에서 더 많은 테스트를 수행할 수 있고 더 자주 실행할 수 있다.
4. 유지 보수성
유지 보수성은 테스트의 유지 비용을 평가하는 속성으로 두 가지 주요 요소로 구성된다.
첫 번째는 '테스트가 얼마나 이해하기 쉬운가'이다. 테스트는 코드 라인이 적을수록 더 읽기 쉬우며, 크기가 작을수록 변경하기도 쉽다. 물론 이는 라인 수를 줄이려고 테스트 코드를 인위적으로 압축하지 않는다고 가정할 때다.
두 번째는 '테스트가 얼마나 실행하기 쉬운가'이다. 테스트가 데이터베이스 연결이나 외부 API 통신 등 프로세스 외부에 의존하고 있다면, 그 연결을 설정하거나 문제를 해결하는 데 추가적인 비용이 발생하게 된다.
오류 유형
오류 유형은 테스트 결과와 기능의 동작 여부에 따라 아래와 같이 네 가지로 나눌 수 있다. 거짓 음성은 기능이 제대로 동작하지 않는데 테스트가 통과되는 것을 의미한다. 이는 회귀 방지를 통해 방지할 수 있다. 거짓 양성은 기능이 올바르게 동작하는데 테스트가 실패하는 것을 의미한다. 이는 리팩토링 내성을 통해 방지할 수 있다.
오류 유형 | 기능 | ||
동작 | 고장 | ||
테스트 결과 | 통과 | 올바른 추론 (참 음성) | 2종 오류 (거짓 음성) |
실패 | 1종 오류 (거짓 양성) | 올바른 추론 (참 양성) |
거짓 양성과 거짓 음성은 이렇게 이해하면 쉬울 것 같다. ‘거짓’은 작성한 테스트 코드가 잘못 되었음을 의미한다. 즉, 테스트가 실패해야 하는데 실패하지 않았고, 통과해야 하는데 통과하지 않았다는 것을 의미한다. ‘양성’은 테스트 코드가 반응한 것을 의미하고, ‘음성’ 테스트 코드가 반응하지 않은 것을 의미한다.
그래서 ‘거짓 양성’은 기능이 동작하는데 테스트 코드가 반응했기 때문에 잘못 되었음을 의미하고, ‘거짓 음성’은 기능이 동작하지 않는데 테스트 코드가 반응하지 않았기 때문에 잘못 되었음을 의미한다.
이상적인 테스트
앞에서 제시한 네 가지 특성을 만족하는 테스트 코드가 가장 이상적이다. 하지만, 네 가지 특성을 모두 최대화하는 것은 불가능하다. 처음 세 가지 특성인 회귀 방지, 리팩토링 내성, 빠른 피드팩은 상호 베타적이기 때문이다. 그렇기 때문에 세 가지 특성 중 하나를 희생하여 나머지 둘을 최대화한다.
엔드 투 엔드 테스트는 많은 코드를 테스트하므로 회귀 방지를 만족하고, 리팩토링 내성이 우수하지만, 느린 속도로 인해 빠른 피드백을 받기 어렵다. 반대로 간단한 테스트는 빠른 피드백을 받을 수 있고, 코드가 적기 때문에 리팩토링 내성이 우수하지만, 회귀 방지를 만족하지 못한다. 또 다른 예로, 깨지기 쉬운 테스트는 회귀 방지를 만족하고, 피드백이 빠르지만, 리팩토링 내성이 떨어진다. 예를 들면, Repository에서 데이터를 가져오는 코드를 테스트 하기 위해서 SQL문을 검증하는 것을 생각해볼 수 있는데, 이러한 코드는 동일한 동작을 하는 다른 SQL문을 사용하면 테스트가 실패한다. 즉 거짓 양성을 보인다.
이 책에서는 리팩토링 내성을 최대한 높이는 것을 목표로 하고, 회귀 방지와 빠른 피드백 사이에서 선택해야 한다고 말한다. 왜냐하면, 회귀 방지와 빠른 피드백은 조절이 가능하지만, 리팩토링 내성은 있거나 없는 이진 선택이기 때문이다.
목(Mock)과 스텁(Stub)
목(Mock)은 외부로 나가는 상호작용을 모방하고 검사하는 데 도움이 된다. 이러한 상호 작용은 SUT가 상태를 변경하기 위한 의존성을 호출하는 것에 해당한다. 예를 들면, 이메일 발송과 같이 사이드 이펙트를 초래하는 상호 작용을 말한다.
스텁(Stub)은 내부로 들어오는 상호 작용을 모방하는 데 도움이 된다. 이러한 상호 작용은 SUT가 입력 데이터를 얻기 위한 의존성을 호출하는 것에 해당한다. 예를 들면, 데이터베이스에서 데이터를 검색하는 것과 같이 내부로 들어오는 상호 작용을 말하며 사이드 이펙트를 일으키지 않는다.
목(Mock)이라는 용어는 도구로서의 목과 테스트 대역으로서의 목을 구분해야 한다. 도구로서의 목은 테스트 대역으로서의 목과 스텁을 생성할 수 있기 때문이다.
'테스트 코드' 카테고리의 다른 글
단위 테스트 (3) - 테스트 코드 작성 스타일 (0) | 2025.05.05 |
---|---|
단위 테스트 (1) - 테스트를 바라보는 관점 (0) | 2025.03.31 |