단위 테스트 (블라디미르 코리코프, 2021) 독후감
단위 테스트(Unit Testing)에 관한 책을 한 권만 꼽는다면 이 책을 꼽고 싶다. 늦게 알아서 억울한 책이다. 예제가 C#이지만 자신이 사용하는 언어와 상관없이 읽으면 도움이 되는 내용들로 가득하다.
모든 코드를 테스트할 수 있을까? 불가능해 보인다. 그래서 테스트를 짜면 가장 효과가 좋은 코드부터 테스트를 짜야 한다. 테스트 커버리지 100%를 달성하는 목표를 가지고 모든 코드에 테스트를 추가해도 테스트를 짜는 순서는 똑같다. 중요하고 효과가 좋은 코드부터 테스트를 짜야 한다. 책에서는 코드 유형을 나누고 테스트를 짤 때 어떻게 접근해야 하는지를 설명한다.
구현 세부 사항(Implementation details) 테스트를 피해야 한다. 안쪽 코드를 리팩터링 했는데, 깨지지 않아야 할 테스트가 우르르 깨지는 경험을 했다. 이게 다 리팩터링 내성(Resistance to Refactoring)이 없어서다. 그럼 어떤 코드를 테스트해야 하나? 식별할 수 있는 동작(Observable behavior)이 좋은 기준이 된다. 식별할 수 있는 동작을 기준으로 테스트를 짜면 리팩터링 내성이 높은 테스트를 짤 수 있다.
단위 테스트(Unit Testing)에서 ’단위’는 어떻게 정의해야 할까? OOP(Object-Oriented Programming) 언어라면 클래스(Class)일까? 고전파와 런던파에서 정의하는 단위에 대한 설명도 도움이 됐다. 난 동작의 단위로 테스트하는 고전파를 선호한다.
테스트에서 프로덕션 코드를 대신해서 사용하는 테스트 대역(Test double)을 목(Mock)과 스텁(Stub)으로 구분지어서 설명한다. ’xUnit 테스트 패턴 (제라드 메스자로스, 2010)’ 책에서는 테스트 대역을 더미(Dummy), 스텁, 스파이(Spy), 목, 페이크(Fake)로 다섯 가지로 분류했는데, 좀 과하긴 하다.
데이터베이스를 사용하는 코드를 테스트한다면 목(Mock)으로 검증해야 할까? 이것도 식별할 수 있는 동작(Observable behavior)이 좋은 기준이 된다. 데이터베이스와 같이 프로세스 외부 의존성도 구현 세부 사항에 해당하면 목을 쓰지 않고 테스트한다. 만약 SMTP나 시스템 버스처럼 다른 애플리케이션과 같이 쓰는 프로세스 외부 의존성이라면 목(Mock)을 사용해야 한다.
독후감을 적으면서 배운 내용을 정리하려고 했는데, 너무 많았다. 그만큼 많이 배운 책이다.
책을 읽으며 배우다
- 단위 테스트가 의미 있는 코드 유형은 뭘까? 통합 테스트는?
- ’식별할 수 있는 동작(Observable behavior)’ - 테스트 대상 코드를 구분하는 기준
- 단위 테스트(Unit Testing)에서 단위의 경계는 무엇인가? (feat. 고전파와 런던파)
- 목(Mock)과 스텁(Stub)은 뭐가 다른가
- 프로세스 외부 의존성을 가진 데이터베이스는 무조건 목(Mock)으로 대체해야 하는가?
밑줄
- 커버리지 지표는 괜찮은 부정 지표이지만 좋지 않은 긍정 지표다. p.37
- 테스트는 코드의 단위를 검증해서는 안 된다. 오히려 동작의 단위, 즉 문제 영역에 의미가 있는 것, 이상적으로는 비즈니스 담당자가 유용하다고 인식할 수 있는 것을 검증해야 한다. 동작 단위를 구현하는 데 클래스가 얼마나 필요한지는 상관없다. 단위는 여러 클래스에 걸쳐 있거나 한 클래스에만 있을 수 있고, 심지어 아주 작은 메서드가 될 수 있다. p.70
- 테스트당 하나의 검증을 갖는 지침을 들어봤을 것이다. 이전 장에서 다뤘던 전제, 즉 가능한 한 가장 작은 코드를 목표로 하는 전제에 기반을 두고 있다. 이미 알고 있듯이 이 전제는 올바르지 않다. 단위 테스트의 단위는 동작의 단위이지 코드의 단위가 아니다. 단일 동작 단위는 여러 결과를 낼 수 있으며, 하나의 테스트로 그 모든 결과를 평가하는 것이 좋다. p.86
- 프로그래밍을 하는 삶에 있어 불행한 사실은 코드가 자산이 아니라 책임이라는 점이다. 코드베이스가 커질수록 잠재적인 버그에 더 많이 노출된다. 그렇게 때문에 회귀에 대해 효과적인 보호를 개발하는 것이 중요하다. 이러한 보호가 없다면 프로젝트가 오랫동안 성장할 수 없으며 점점 더 많은 버그가 쌓일 것이다. p.115
- 회귀 방지는 테스트 중에 실행되는 코드 양에 대한 함수다. p.317
- 테스트에서 발생하는 거짓 양성(false positive)의 수는 테스트 구성 방식과 직접적인 관련이 있다. 테스트와 테스트 대상 시스템(SUT)의 구현 세부 사항이 많이 결합할수록 허위 정보가 더 많이 생긴다. 거짓 양성이 생길 가능성을 줄이는 방법은 해당 구현 세부 사항에서 테스트를 분리하는 것뿐이다. p.119
- 완전히 통제권을 가진 프로세스 외부 의존성에 목(Mock)을 사용하면 깨지기 쉬운 테스트로 이어진다. 데이터베이스에서 테이블을 분할하거나 저장 프로시저에서 매개변수 타입을 변경할 때마다 테스트가 빨간색이 되는 것을 아무도 원하지 않는다. 데이터베이스와 애플리케이션은 하나의 시스템으로 취급해야 한다. p.179
- 클래스 간의 통신을 검증하는 것은 두뇌의 뉴런이 서로 통과하는 신호를 측정해 사람의 행동을 유추하는 것과 유사하다. 이러한 세부 수준은 너무 세밀하다. 중요한 것은 클라이언트 목표로 거슬러 올라갈 수 있는 동작이다. 클라이언트는 도움을 청할 때 두뇌의 어떤 뉴런이 켜지는지 신경 쓰지 않는다. 클라이언트에게 중요한 것은 도움뿐이다. p.179
- 목(Mock)을 사용할 때 항상 다음 지침을 따르자. 시스템 끝에서 비관리 의존성과의 상호 작용을 검증하라. p317
- 도메인 모델에 대한 테스트는 단위 테스트 범주에 속하며, 컨트롤러를 다루는 테스트는 통합 테스트다. 목(Mock)은 비관리 의존성에만 해당하며 컨트롤러만 이러한 의존성을 처리하는 코드이기 때문에 통합 테스트에서 컨트롤러를 테스트할 때만 목을 적용해야 한다. p.325
- 코드가 더 중요해지거나 복잡해질수록 협력자(collaborator)는 더 적어야 한다. p.231