TDD 패턴
- 5.1 TDD 패턴 — 테스트 먼저, 단언 먼저
- 5.2 빨간 막대 패턴 — 시작과 학습
- 5.3 테스트 작성 기법 — 모의 객체와 로그
- 5.4 초록 막대 패턴 — 가짜에서 진짜로
- 5.5 xUnit 패턴과 디자인 패턴
- 5.6 리팩토링 패턴과 마무리
TDD에서 자주 쓰는 패턴들을 카탈로그처럼 정리하면 어떨까? 3부는 1부와 2부에서 써먹었던 기법들을 명시적으로 이름 붙이고 체계화한 파트야.
테스트를 언제 작성하냐면, 코드를 작성하기 전에 써. 이게 TDD의 가장 기본 규칙이지. 왜 먼저 작성하냐면, 테스트를 나중에 쓰면 스트레스가 쌓이고, 스트레스가 쌓이면 테스트를 안 쓰게 되고, 테스트를 안 쓰면 더 불안해지는 악순환에 빠지거든. 테스트를 먼저 쓰면 이 악순환이 깨져. 테스트를 작성할 때는 단언(assert)부터 먼저 써. 결론부터 쓰고 거꾸로 올라가는 거야. assertEquals(result, expected)를 먼저 적고, 그 다음에 result를 만드는 코드를 쓰고, 그 다음에 필요한 객체를 생성하지. 결론부터 쓰면 테스트의 의도가 명확해져. 테스트에 사용하는 데이터도 의미 있게 골라야 해. 1이나 2 같은 숫자를 쓸 때 같은 값을 여러 곳에 쓰면 "이게 맞아서 통과한 건지, 우연히 같아서 통과한 건지" 구분이 안 되거든. 테스트는 다른 사람에게 의도를 전달하는 문서야. 읽기 쉬운 테스트가 좋은 테스트지.
빨간 막대 패턴은 실패하는 테스트를 어떻게 잘 작성하느냐에 대한 거야. 어디서부터 시작할지 모르겠을 때, 가장 간단한 테스트부터 써. "빈 리스트를 넣으면 빈 리스트가 나온다" 수준이면 충분해. 일단 빨간 막대를 보고 초록 막대로 바꾸는 경험을 하면 다음 단계가 보이거든. 할 일 목록에서 다음 테스트를 고를 때는 한 단계만 더 나아가는 걸 골라. 너무 큰 걸음을 떼면 초록 막대까지 가는 데 오래 걸리고, 오래 걸리면 불안해지니까. 설명 테스트라는 것도 있어. 다른 사람에게 구현을 설명할 때 테스트를 사용하는 거지. 테스트는 실행 가능한 문서거든. 외부 라이브러리를 처음 쓸 때도 학습 테스트를 작성해서 동작을 확인하면 좋아. 나중에 라이브러리가 업그레이드되면 이 테스트들이 호환성 체크도 해주지.
테스트를 작성하는 구체적인 기법도 있어. 테스트가 너무 크면 쪼개야 해. 큰 테스트를 작성했는데 빨간 막대가 나오고 뭐가 문제인지 모르겠으면, 그 테스트의 일부분만 검증하는 자식 테스트를 만들어. 자식 테스트가 초록이 되면 다시 큰 테스트로 돌아가지. **모의 객체(Mock Object)**는 DB나 네트워크 같은 느리고 불안정한 자원에 의존하는 코드를 테스트할 때 쓰는 거야. 진짜 자원 대신 가짜를 사용해서 "이 쿼리가 오면 이 값을 돌려줘"라고 해두면 빠르고 확실하게 테스트할 수 있지. 로그 문자열은 메서드 호출 순서를 검증하고 싶을 때 쓰는 패턴이야. 2부에서 이미 써봤지. 각 메서드가 호출될 때 로그에 자기 이름을 추가하고, 마지막에 로그를 검증해. 크래시 테스트 더미는 예외 경로를 테스트할 때 쓰는데, 특정 메서드에서 무조건 예외를 던지는 특수 객체를 만드는 거야. "파일 쓰기가 실패하면 어떻게 되는가"를 테스트하려면, write()에서 항상 IOException을 던지는 가짜 스트림을 만들면 돼.
초록 막대 패턴은 빨간 막대를 초록으로 바꾸는 전략이야. 세 가지가 있지. 첫째, 가짜로 구현하기. 상수를 반환해서 초록 막대를 본 다음, 점진적으로 변수를 사용하는 코드로 바꿔. 왜 이런 바보 같은 짓을 하냐면, 심리적으로 초록 막대를 보면 안심이 되고, 중복 제거 과정에서 올바른 구현으로 자연스럽게 수렴하거든. 둘째, 삼각측량. 테스트가 하나일 때는 하드코딩으로 통과할 수 있지만, 두 번째 테스트를 추가하면 일반화할 수밖에 없어. 셋째, 명백한 구현. 답을 알고 있으면 그냥 바로 구현해. 단, 명백한 구현을 했는데 빨간 막대가 나오면 가짜로 구현하기로 후퇴해서 작은 단계를 밟아. 자신감이 있을 때는 큰 걸음, 불안할 때는 작은 걸음이야. 컬렉션을 다룰 때는 하나에서 여럿으로 확장하는 전략도 써. 먼저 원소 하나에 대해 동작하게 만들고, 그 다음에 컬렉션으로 확장하지.
xUnit 패턴은 테스트 프레임워크를 쓸 때의 실전 팁이야. 단언은 구체적으로 써. assertTrue(result > 0) 대신 assertEquals(3, result)가 나아. 실패했을 때 메시지가 더 도움이 되거든. **픽스처(Fixture)**는 여러 테스트가 공통으로 사용하는 객체인데, setUp()에서 생성해서 인스턴스 변수에 저장해. 단, 픽스처가 너무 복잡해지면 테스트를 읽기 어려워지니까 주의해야 해. 테스트 메서드 이름은 의도를 드러내게 지어. testAdd 보다 testSumOfTwoDollars가 낫지. 실패했을 때 이름만 봐도 뭐가 깨졌는지 감이 와야 하니까.
TDD를 하면서 자주 등장하는 디자인 패턴도 정리해 놓자. 커맨드 패턴은 연산 자체를 객체로 만드는 거야. 1부에서 Expression이 바로 이 패턴이었지. 값 객체는 한번 생성하면 값이 바뀌지 않는 불변 객체고, Dollar가 그랬어. 생성자에서 모든 값을 설정하고 이후로는 절대 변경하지 않는 게 규칙이야. 널 객체는 null 체크를 없애고 싶을 때 "아무것도 안 하는" 특수 객체를 사용하는 거고, 컴포지트는 하나의 객체와 컬렉션을 동일하게 다루는 패턴이야. TestSuite가 이 패턴이었지. 임포스터는 같은 인터페이스를 구현하지만 전혀 다른 일을 하는 객체인데, 모의 객체가 대표적이야. 핵심은 TDD는 자연스럽게 좋은 패턴으로 이끈다는 거야. 패턴을 먼저 정하고 코드를 짜는 게 아니라, 코드가 패턴을 필요로 할 때 적용하지.
리팩토링 패턴도 중요해. 차이점 일치시키기는 비슷하지만 다른 두 코드를 합칠 때, 먼저 둘을 최대한 비슷하게 만든 다음 합치는 거야. 1부에서 Dollar.times()와 Franc.times()를 합칠 때 썼지. 변화 격리하기는 큰 메서드에서 변경할 부분만 따로 뽑아내는 기법이고. 메서드 추출은 가장 자주 쓰는 리팩토링인데, 긴 메서드에서 의미 있는 덩어리를 별도 메서드로 뽑아. 반대로 메서드 인라인은 메서드가 너무 작거나 이름이 내용보다 복잡할 때 호출하는 쪽에 코드를 직접 넣는 거야. 간접 참조가 항상 좋은 건 아니거든. 인터페이스 추출은 두 번째 구현이 필요해지는 순간 하는 거지.
마지막으로 실무에서 부딪히는 현실적인 질문들. 얼마나 테스트해야 하느냐? 켄트 벡의 답은 명쾌해 -- 불안한 만큼 테스트해. 코드에 자신감이 있으면 테스트를 덜 써도 되고, 불안하면 더 써. 100% 커버리지가 목표가 아니야. 테스트의 목적은 자신감이지. TDD는 개인 실천법이기도 하지만 팀 전체의 소통 도구이기도 해. 테스트는 실행 가능한 명세서거든. 새로운 팀원이 왔을 때 테스트 코드를 읽으면 시스템이 어떻게 동작하는지 이해할 수 있지. 패턴을 알고 있으면 TDD가 더 효과적이야. 패턴은 "이 방향으로 가면 되겠다"는 힌트를 주거든. 하지만 패턴부터 적용하라는 건 아니야. 테스트가 이끄는 대로 따라가되, 패턴을 알고 있으면 더 빨리 갈 수 있어. 켄트 벡의 마지막 메시지 -- TDD는 기법이 아니라 습관이야. 처음에는 어색하지만 반복하면 자연스러워져. "깨끗한 코드가 동작하게 하라(Clean code that works)". 이것이 TDD의 궁극적 목표야. 동작하게 만들고, 그 다음에 깨끗하게 만들어. 이 순서를 지키는 한, 이미 TDD를 하고 있는 거야.
정리
5장 읽고 기억할 거 세 가지:
- 가짜로 구현하기, 삼각측량, 명백한 구현 -- 초록 막대로 가는 세 가지 전략이야. 자신감이 있을 때는 큰 걸음, 불안할 때는 작은 걸음을 밟지
- 불안한 만큼 테스트해. 100% 커버리지가 목표가 아니라 자신감이 목표야. 모의 객체, 로그 문자열, 크래시 테스트 더미 같은 기법으로 외부 의존성과 예외 경로도 테스트하고
- "깨끗한 코드가 동작하게 하라". 이 한 문장이 TDD의 전부야. 동작하게 만들고, 깨끗하게 만들어. 패턴은 코드가 필요로 할 때 자연스럽게 나오지