'Test Driven Development: By Example' 책 내용 정리

들어가며

  • 어디까지나 개인 노트에 정리했던 내용을 그대로 옮긴 것이라 가독성이 많이 떨어질 수 있습니다.
  • 책을 읽고 계신 분이, 혹은 읽어보신 분이 맥을 되짚는데 참고가 되길 바랍니다.

책은 1부에서 실전 튜토리얼을 진행하고, 2부는 xUnit 활용 예, 3부에서 TDD를 활용한 리팩터링을 다룬다. 여기서 xUnit 파트는 제외한다.

TDD에서는 두 가지 규칙을 따른다.

  • 자동화된 테스트가 실패할 경우에만 새로운 코드를 작성한다.
  • 중복을 제거한다.

이런 방식으로 코드를 작성하기 위해 테스트를 쉽게 만들어야 하고, 결과적으로 응집도는 높고 결합도는 낮은 컴포넌트들로 구성되게끔 설계하게 된다. 실패하는 테스트를 통과시키기 위해 필요한 만큼 코딩하여 더 정확한 추정, 더 밀집된 단위의 협력이 가능해진다.

1부 핵심 내용 요약

  • TDD의 리듬

    1. 재빨리 테스트를 하나 추가한다.
    2. 모든 테스트를 실행하고 새로 추가한 것이 실패하는지 확인한다.
    3. 코드를 조금 바꾼다.
    4. 모든 테스트를 실행하고 전부 성공하는지 확인한다.
    5. 리팩토링을 통해 중복을 제거한다.
  • TDD의 주기

    1. 테스트를 작성한다: 원하는 인터페이스를 구상하고 어떤 식으로 사용되길 원하는지 본다.
    2. “가능한 한 빨리” 실행 가능하게 만든다. 구현 디테일은 배제하고 일단 돌아가게 만들어본다.

      1. 가짜로 구현하기: 상수를 반환시키고 단계적으로 변수로 바꾸어 나간다.
      2. 명백한 구현 사용하기: 실제 구현 입력
    3. “올바르게” 만든다. 중복 등을 제거하고 제대로 된 방식으로 소프트웨어를 만들어보자.
  • “작동하는” “깔끔한” 코드를 얻기 위해 두 부분을 분리해야 한다. 그래서 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를 권장한다.

안도형(Dohyung Ahn)
삽질을 하고, 글을 남기면서 다른 사람들과 함께 자라고 싶어하는 프론트엔드 개발자입니다. 더 좋은 코드와 설계를 항상 고민하며 지식을 어떻게 효율적으로 습득하고, 어떻게 잘 나눌 수 있을지도 고민합니다.

GitHubTwitterFacebook