들어가며
- 어디까지나 개인 노트에 정리했던 내용을 그대로 옮긴 것이라 가독성이 많이 떨어질 수 있습니다.
- 책을 읽고 계신 분이, 혹은 읽어보신 분이 맥을 되짚는데 참고가 되길 바랍니다.
책은 1부에서 실전 튜토리얼을 진행하고, 2부는 xUnit 활용 예, 3부에서 TDD를 활용한 리팩터링을 다룬다. 여기서 xUnit 파트는 제외한다.
TDD에서는 두 가지 규칙을 따른다.
- 자동화된 테스트가 실패할 경우에만 새로운 코드를 작성한다.
- 중복을 제거한다.
이런 방식으로 코드를 작성하기 위해 테스트를 쉽게 만들어야 하고, 결과적으로 응집도는 높고 결합도는 낮은 컴포넌트들로 구성되게끔 설계하게 된다. 실패하는 테스트를 통과시키기 위해 필요한 만큼 코딩하여 더 정확한 추정, 더 밀집된 단위의 협력이 가능해진다.
1부 핵심 내용 요약
- TDD의 리듬
- 재빨리 테스트를 하나 추가한다.
- 모든 테스트를 실행하고 새로 추가한 것이 실패하는지 확인한다.
- 코드를 조금 바꾼다.
- 모든 테스트를 실행하고 전부 성공하는지 확인한다.
- 리팩토링을 통해 중복을 제거한다.
- TDD의 주기
- 테스트를 작성한다: 원하는 인터페이스를 구상하고 어떤 식으로 사용되길 원하는지 본다.
- ”가능한 한 빨리” 실행 가능하게 만든다. 구현 디테일은 배제하고 일단 돌아가게 만들어본다.
- 가짜로 구현하기: 상수를 반환시키고 단계적으로 변수로 바꾸어 나간다.
- 명백한 구현 사용하기: 실제 구현 입력
- ”올바르게” 만든다. 중복 등을 제거하고 제대로 된 방식으로 소프트웨어를 만들어보자.
- ”작동하는” “깔끔한” 코드를 얻기 위해 두 부분을 분리해야 한다. 그래서 TDD를 하며 먼저 “작동하는 코드” 부분을 해결하고 그 다음 “깔끔한 코드” 부분을 해결하는 것이다.
- 삼각측량 전략
- 하나의 테스트에 대해 두 번째 예제를 작성하여 더 정밀하게 유추해보도록 한다.
- 일반적으로 답을 구하는게 가능하다면 바로 해결해버리면 되지만, 삼각측량을 통해 새로이 리팩토링 할 부분을 발견하기도 한다.
- 충분한 테스트가 갖춰지지 못한 곳에서 리팩토링을 하게 되는 경우, 있으면 좋을 것 같은 테스트를 작성하라. 그렇지 않으면 결국에는 리팩토링하다 뭔가 깨트릴 것이다.
Dollar
,Franc
클래스에서 공통적인 부분을 추출하여Money
라는 수퍼클래스를 만들었고, 두 하위 클래스의 중복되는 부분을 끌어 올리다보니 최종적으로Money
클래스만 남기고 나머지를 지울 수 있게 되었다.- 전체 기능에 대한 스토리를 상정하고, 기능이 변경되면서 일종의 체크리스트를 계속 관리하며 지워나간다. 지워나가는 방법은 위에 서술했듯 Red -> Green -> Refactor 의 사이클을 도는 것이다.
- 다른 통화끼리 더하는 기능을 개발하려고 할 때, 큰 테스트를 작은 테스트로 줄여서 점진적으로 접근하려고 시도했다. ($5 + 10CHF를 구현하기에 앞서 $5 + $5가 구현되도록 만드는 것)
- 그리고 이 기능을 구현하기 위해 가능한 메타포를 신중히 생각하고, 기존의 테스트를 재작성하는 것부터 시작했다.
- 클래스를 명시적으로 검사하는 코드가 있을 경우 항상 다형성을 활용하도록 바꾸는 것이 좋다.
- ”다음에 할 일은 무엇일까?” -> “어떤 테스트들이 추가로 더 필요할까?” 라는 것이다.
- 할 일 목록이 비게 되면 그때까지 설계한 것을 검토하기 적절한 시기가 된다.
- 말과 개념이 서로 잘 통하는가?
- 현재의 설계로는 제거하기 힘든 중복이 있는가?
- 메타포를 구상하는 것의 강력함을 상기할 것
- TDD로 자연히 생긴 테스트 외에 다음과 같은 테스트들을 고려하는 것도 잊으면 안된다.
- 성능 테스트
- 스트레스 테스트
- 사용성 테스트
- TDD를 하면서 계속 상기해야 할 핵심 내용 3가지
- 테스트를 확실히 돌아가게 만드는 세 가지 접근법: 가짜로 구현하기, 삼각측량법, 명백하게 구현하기
- 설계를 주도하기 위한 방법으로 테스트 코드와 실제 코드 사이의 중복을 제거하기
- 상황에 따라 테스트 사이의 간격을 조절하는 능력 마련하기
3부 핵심 내용 요약
테스트를 할 때 기본 전략에 관한 질문
- 테스트한다는 것은 무엇을 뜻하는가?
- 여기서는 “자동화 테스트” 를 뜻한다. 수동 테스트는 긍정적인 피드백을 가져다 주기 어렵고 ‘테스트 할 시간이 없다’ 라는 핑계거리를 제공하기에 아주 좋다.
- 테스트를 실행할 때 서로 영향이 없어야 한다. 그리고 테스트가 충분히 빨라서 직접, 자주 실행할 수 있도록 만들어야 한다. 격리된 테스트는 테스트의 실행 순서를 독립적으로 만들 뿐 아니라 주어진 문제를 작은 단위로 분리하도록 만든다. 결과적으로 응집도가 높고 결합도는 낮은 구성을 고민하게 되는 것이다.
- 테스트를 언제 해야 하는가?
- 테스트 대상이 되는 코드를 작성하기 직전에 작성하는 것이 좋다. 코드를 작성한 뒤에 다른 일들을 생각하면 스트레스와 테스트 사이에 양성 피드백 고리가 발생하여 테스트는 더더욱 작성하지 않을 확률이 높다.
- 테스트 작성 시 단언은 언제쯤 작성할까? 단언을 제일 먼저 작성하라. 구현을 고려하지 않고 테스트를 먼저 작성하는 것과 비슷한 원리이다.
- 테스트할 로직을 어떻게 고를 것인가?
- 시작하기 전에 작성해야 할 테스트 목록을 모두 적는다.
- 구현할 필요가 있는 모든 동작의 사용 예들을 적고, 이미 존재하지 않는 동작은 해당 동작의 비어있는 버전을 리스트에 적고, 마지막으로 깔끔한 코드를 얻기 위해 이번 작업을 완전히 끝내기 전에 반드시 해야 할 리팩토링 목록을 적는다.
- TDD를 진행하면서 작성한 코드들은 새로운 테스트가 필요하게 만든다. 이 목록도 리스트에 적는다.
- 테스트할 데이터를 어떻게 고를 것인가?
- 테스트를 읽을 때 쉽고 따라하기 좋을 만한 데이터를 사용하라.
- 데이터 간에 차이가 있다면 그 속에 어떠한 의미가 있어야 한다.
- 실제 세상에서 얻어진 실제 데이터를 사용하는 것도 유용하다.
- 테스트 자체에 예상되는 값과 실제 값을 포함하고 이 둘 사이의 관계를 드러내기 위해 노력해야 한다. 코드를 읽는 다름 사람들(미래의 나 포함)도 생각해야 하기 때문이다.
빨간 막대 패턴: 테스트를 언제 어디에 작성할지, 테스트 작성을 언제 멈출지
- 목록에서 다음 테스트를 고르는 기준은? 새로운 무언가를 가르쳐줄 수 있으며 구현할 수 있다는 확신이 드는 테스트를 고르는 것이다.
- 상향식, 하향식 개발 모두 TDD 프로세스를 효과적으로 설명해줄 수 없다. 그 대신 ‘성장(아는 것에서 모르는 것으로)’ 이라는 피드백 고리를 활용한다.
- 어떤 테스트부터 시작해야 하는가?
- 동작이 아무 일도 하지 않는 경우를 먼저 테스트한다. 첫 걸음으로 현실적인 테스트를 작성하게 된다면 상당히 많은 문제를 한 번에 해결해야 하는 상황을 겪는다. 그러면 Red/Green/Refactor 고리를 빨리 돌릴 수 없다.
- 뭔가를 가르쳐 줄 수 있으면서 빠르게 구현할 수 있는 테스트를 선택하라.
- 테스트를 통해 설명을 요청하고 테스트를 통해 설명하라.
- 외부 모듈 혹은 패키지에 대한 테스트는 그 모듈의 새로운 기능을 처음으로 사용해보기 전에 작성할 수도 있다.
- 주제에 무관한 아이디어가 떠오르면 이에 대한 테스트를 할 일 목록에 적어놓고 다시 주제로 돌아온다. 새 아이디어느 존중하되 주의를 흩뜨리지 않는 것이 좋다.
- 장애가 발생하면 그 장애로 인하여 실패하는 테스트를 작성하고 테스트가 통과하면 장애가 수정되었다고 볼 수 있는 테스트를 간단하게 작성하라. 시스템 장애를 손쉽게 격리시킬 수 없다면 리팩토링이 필요한 시점이라는 뜻이다.
- 적절한 휴식은 필수다.
- 길을 잃은 느낌이 들 땐 다 지우고 처음 부터 다시 하는 것도 방법이 될 수 있다.
- 책상은 아무리 싸구려라도 의자는 좋은 것을 써야 한다.
테스팅 패턴: 더 상세한 테스트 작성법
- 자식 테스트
- 지나치게 큰 테스트를 돌아가도록 만들기 위해 깨지는 부분에 해당하는 작은 테스트 케이스를 작성하여 그 케이스가 실행되도록 한다. 그리고 다시 원래의 큰 테스트 케이스로 돌아온다. Red/Green/Refactor 리듬의 성공은 아주 중요하다.
- Mock Object
- 복잡한 리소스에 의존하는 객체를 테스트 하는 경우 활용
- 성능적으로 유리하고 가독성을 확보할 수도 있다.
- 모든 객체의 가시성에 대해 고민하도록 격리하여 설계에서 커플링이 감소하도록 만든다. 하지만 Mock 객체가 실제 객체와 동일하기 동작하지 않을 경우는 위험 요소가 하나 추가되는 것이다.
- 셀프 션트
- 테스트 케이스를 위해 별도의 인터페이스를 구축하는 것이 아니라 테스트 케이스 안에서 인터페이스를 구축하고 이 안에서 통신하는 패턴을 일컫는다.
- 테스트의 가독성이 올라가는 효과도 얻을 수 있다. 그렇지 않으면 블랙박스화된 객체의 내부를 들여다봐야 하기 때문이다.
- 로그 문자열
- 메세지의 호출 순서가 올바른지 검사하기 위해 메세지가 호출 될때마다 로그 문자열에 추가되도록 만드는 방식으로 구현할 수 있다.
- 특히 옵저버를 구현하고, 이벤트 통보가 원하는 순서대로 발생했는지 확인할 때 유용하다.
- 크래시 테스트 더미
- 발생하기 힘든 에러 상황을 테스트할 때는 실제 작업을 수행하는 대신 예외를 발생시키기만 하는 특수한 객체를 만들어 호출하게 시킨다.
- 수많은 에러 상황을 모두 테스트하기보다 작동하길 원하는 부분에 대해서만 하면 되는 것이다.
- 깨진 테스트
- 혼자 프로그래밍 세션을 끝낼 때 마지막 테스트가 실패한 채로 마무리하는 것이 좋다. 글을 작성할 때 반 쪽짜리 문장을 작성해두며 마친 뒤, 다시 문장을 들여다볼 때 그 전에 무슨 생각을 하며 문장을 썼는지 떠올리는 기법을 응용하는 것이다.
- 깨끗한 체크인
- 팀 단위의 프로그래밍 세션을 끝낼 때는 모든 테스트가 성공한 상태로 끝마치는 것이 좋다. 팀 프로젝트에서 다시 프로그래밍 세션을 시작할 때는 자신이 마지막으로 코딩한 다음부터 무슨 일이 있었는지 세밀하게 알 수 없기 때문이다.
- 따라서 안심이 되고 확신이 있는 상태에서 시작하도록 만들기 위해 모든 테스트가 돌아가는 상태로 만들어 두어야 한다.
초록 막대 패턴: 코드가 테스트를 통과하게 만들기 위한 패턴들
- 가짜로 구현하기(진짜로 만들기 전까지만)
- 실패하는 테스트를 만든 후 첫 번째 구현은 상수를 반환하게 하는 것.
- 일단 테스트를 통과시키고 단계적으로 상수를 변수를 사용하는 수식으로 변형한다.
- 심리적으로 초록을 본 상황에서 좀 더 확신을 갖고 리팩토링이 가능하다.
- 다음 작업의 범위를 상정하면서 이르게 혼동하게 되는 일을 방지할 수 있다.
- 삼각측량
- TDD 기반 추상화 과정을 거칠 때, 예가 두 개 이상일 때만 추상화를 하라.
- 어떤 계산을 어떻게 해야 올바르게 추상화할 것인지 감 잡기 어려운 경우 활용한다. 그 외에는 명백한 구현이나 가짜로 구현하기를 주로 활용한다.
- 명백한 구현
- 단순한 연산들은 그냥 구현해버려라.
- ‘제대로 동작하게’ 만드는 것과 ‘깨끗하게 만드는’ 것을 동시에 하기엔 너무 일이 많을 수 있으니, ‘제대로 동작하는’ 부분을 해결하고 깨끗하게 만들어라.
- 구현하려던 것이 의도적으로 돌아가지 않는다면 더 작은 스텝(가짜 구현, 삼각측량) 등으로 되돌아가면 된다.
- 하나에서 여럿으로
- 컬렉션 값을 다루는 연산은 먼저 컬렉션 없이 구현하고 그 다음 컬렉션을 사용하게 한다.
디자인 패턴
- TDD의 어느 단계에서 사용되느냐에 따라 디자인 패턴도 다양하게 활용된다.
- 커맨드
- 메세지를 보냈다는 것만으로 충분할 떄가 있으니, 메세지를 보낸다는 인터페이스에 관한 구현 및 테스트를 하면 된다. 복잡한 구현을 할 필요가 없다.
- 값 객체
- 객체가 ‘시간의 흐름에 따라 변하는 상태’ 를 가지지 않도록 취급한다. 이뮤터블하게 만든다고 볼 수 있겠다.
- 널 객체
- 널 체크를 매번 하는게 아니라 같은 인터페이스 껍데기를 가졌지만 빈 동작을 수행하는 객체를 리턴하게 만드는 것이다.
- 널 객체 활용의 적절성에 대해서는 여러 이야기가 있으니 취사선택을 할 필요가 있다.
- 템플릿 메서드
- 상위 클래스에서 다른 메서드를 호출하는 내용으로만 이루어진 메서드를 만들고, 하위 클래스에서 각각의 메서드를 서로 다른 방식으로 구현한다.
- 순서에 관련된 내용을 구현할 때 유용할 것이다.
- 초기 설계에 의해 얻어지는 것보다는 경험에 의해 발견되는 것이 좋다.
- 플러거블 객체
- 명시적인 조건문이 중복되는 경우 고려한다.
- 예를 들어 마우스의 각 액션마다 single select, multi select 상태인경우를 고려하는 조건문이 발생한다면 아예 모드 객체를 만들어서 그 모드가 해당 상태를 관리하도록 만들고, 액션들은 그 모드에서 제공하는 인터페이스만 가져다 쓰는 것이다.
- 팩토리 메서드
- 생성자는 표현력과 유연함이 떨어지는 경향이 있다. 그래서 이전 예제 작성 시 도입했던
Money.dollar
같은 템플릿 메서드를 활용할 수 있다. 이로 인해 테스트를 변경하지 않고도 다른 클래스의 인스턴스를 반환하는 유연함을 얻을 수 있다. - 다만 덜 직관적이라는 단점이 있기 떄문에, 유연함이 굳이 필요하지 않다면 직접 생성자를 사용해도 무관하다.
- 생성자는 표현력과 유연함이 떨어지는 경향이 있다. 그래서 이전 예제 작성 시 도입했던
- 컴포지트
- 하나의 객체가 다른 객체 목록의 행위를 조합한 것 처럼 행동하게 만들 때 사용한다.
리팩토링
- 일반적으로 리팩토링은 어떤 상황에서도 프로그램의 의미론을 변경해서는 안 된다. 하지만 TDD에서 우리가 신경 쓰는 부분은 현재 이미 통과한 테스트뿐이다. 예를 들어 상수를 사용하여 통과한 테스트를 변수로 바꾸고 다시 통과시키는 행위도 리팩토링이라 부르는 것이다.
- 이 ‘관측상의 동치성’ 이 성립되려면 충분한 테스트를 갖고 있어야 한다. 여기서 충분한 테스트란 현재 가지고 있는 테스트들에 기반한 리팩토링이 추측 가능한 모든 테스트에 기반한 리팩토링과 동일한 것으로 여겨질 수 있는 상태를 말한다.
- 차이점 일치시키기
- 비슷해 보이는 두 코드를 단계적으로 닮아가게끔 수정하고, 이 둘이 완전히 동일해지면 합친다.
- 작은 단계와 명확한 피드백을 이용하여 불확실한 믿음에 의해 단계를 크게 건너뛰는 방식의 리팩토링을 피한다.
- 변화 격리하기
- 객체나 메서드의 일부를 바꾸려면 바꿔야 할 부분을 격리한다.
- 메서드 추출하기, 객체 추출하기, 메서드 객체 등을 사용하여 격리한다.
TDD 마스터하기
- 단계가 얼마나 커야 하나?
- 각 테스트가 다뤄야 할 범위는 얼마나 넓은가?
- 리팩토링을 하면서 얼마나 많은 중간 단계를 거쳐야 하는가?
- 당연 상황에 따라 다르지만, 리팩토링 초기에는 아주 작은 단계로 작업할 준비가 되어 있어야 한다. 수작업 리팩토링은 에러가 발생하기 쉽고, 그러다 보면 점점 리팩토링을 덜 하게 될 것이다.
- 테스트 할 필요가 없는 것은 무엇인가?
- ”두려움이 지루함으로 변할 때까지 테스트를 만들어라”
- 불신할 이유가 없다면 다른 사람이 만든 코드를 테스트하지 마라. 떄떄로 외부 코드 때문에 자신만의 로직을 더 작성해야 하는 경우도 있다.
- 좋은 테스트를 갖췄는지 여부는 어떻게 알 수 있는가? (어떤 테스트가 설계의 문제점을 보여주는가?)
- 긴 셋업 코드: 객체가 너무 크다는 뜻이므로 나뉠 필요가 있다.
- 셋업 중복: 서로 밀접하게 엉킨 객체들이 너무 많다는 뜻이다.
- 실행 시간이 오래 걸리는 테스트: 실행하는데 오래 걸리면 테스트를 자주 실행하지 않게 되고, 나중에는 테스트가 아예 동작하지 않을 수도 있다. 그리고 작은 부분만 따로 테스트하기 힘들다는 것을 의미한다. 작은 부분만 테스트할 수 없다는 것은 설계 문제를 의미한다.
- 깨지기 쉬운 테스트: 애플리케이션의 특정 부분이 다른 부분에 이상한 방법으로 영향을 끼치는 것이다. 그 영향력을 제거해야 한다.
- 피드백이 얼마나 필요한가? (테스트를 얼마나 작성해야 하나?)
- 어떤 테스트를 작성할 필요가 있을지 없을지는 실패간 평균시간을 얼마나 조심스럽게 측정하는지에 따라 달렸다.
- TDD에서 테스트는 ‘우리가 깊이 신뢰할 수 있는 코드’ 라는 목표를 달성하기 위한 수단이다. 따라서 어떤 구현에 대한 지식이 신뢰할만 하다면 그에 대한 테스트를 작성하지 않을 것이다.
- 테스트를 지워야 할 때는 언제인가?
- 자신감: 테스트를 삭제할 경우 자신감이 줄어들 것 같으면 절대 테스트를 지우지 말아야 한다.
- 커뮤니케이션: 두 개의 테스트가 동일한 부분을 실행하더라도 서로 다른 시나리오를 말한다면 그대로 남겨두어야 한다.
- 위와 다르게 별 부가적인 이득 없는 중복된 테스트가 있다면 덜 유용한 것을 삭제하라.
- 애플리케이션 레벨의 테스트로도 TDD를 할 수 있는가? (실제 사용자가 개입하는 수준의 테스트)
- 픽스쳐를 만들기가 어렵다. 아직 만들지 않은 기능에 대한 테스트를 어떻게 작성하고 실행할 것인가?
- ‘사용자’ 라는 새로운 책임 관점이 포함되면서 테스트를 우선적으로 작성하는데 난이도가 올라간다.
- 테스트와 피드백 사이의 길이가 길다.
- 이러한 이유 때문에 프로그래머 수준의 TDD를 권장한다.