TDD : Test Driven Development

참고문서

TDD ?

tdd_process 프로세스 자체는 굉장히 심플하다.

  1. 직접 코딩하기 전에, 실패하는 테스트 코드를 작성해둔다. 이는 '정답'이 나오는 테스트들을 '양산하는' 것을 방지하기 위해서다.

  2. 코드를 작성하고, 테스트 코드를 통과하는지 확인한다.

  3. 필요하다면 리팩토링한다. 든든한 테스트코드를 작성했다면, '어디가 망가졌는지'를 파악할 수 있으므로, 리팩토링에 자신감을 갖자.

비효율적?

two_states_of_programmer

나도 최근에 테스트 코드를 직접 작성해보면서, 이상과의 괴리감을 좀 느꼈다. 생각만큼 그 '테스트 코드'를 작성하는 것이 쉽지 않았다.

표면 상으로도, 이 또한 코드를 작성하는 것이기 때문에, 추가적인 코드를 위한, 추가적인 시간 소요가 필요하다. 코드를 테스트하기 위한 테스트 코드를 테스트 하기 위한 테스트... 같은 느낌으로 코드를 만들고 있는 내 모습을 볼 수 있었다.

틀린 말은 아니다. 처음 테스트 코드를 작성할 때, 몇가지 노고를 겪는다.

  • 테스트 코드를 작성하는 법 자체
  • 어떤 테스트를 추가해야 하는지

결국, TDD도 러닝커브가 있다. 위 그림처럼 이상적인 프로세스로 보이는 것도, 실제로 해보려면 익숙해지기 위한 시간이 제법 되는 것이다. 한 작성자는 대략 15%-35% 정도 시간이 더 늘어난다고 말했다.

다만, 이것에 익숙해지기 시작하면, 마법같은 일이 일어나는데, 유닛테스트에 익숙해지면서 이전과는 비교가 안될 속도로 코드를 작성하는 것이다.

수동 테스트

TDD라는 개념이고 뭐고, 테스트 코드란게 있는지 조차 몰랐을 적의 내 모습을 떠올려보자.

HTML/CSS, JS를 작성하면서, 제대로 됐는지 확인을 해야한다. 당시에 내가 할 수 있는 유일한 '테스트'는 새로고침을 하고, 직접 버튼을 눌러보고, console.log로 직접 상태를 확인하는 것이었다.

코드 바꾸고, 저장하고, 새로고침하고, 클릭하고, 기다렸다가, 어 안되네, console.log달고, 바꾸고, 저장하고, 아 이것도 안되네.... 콤보

내가 건들고 있는 부분이 비록 한 두 페이지에 불과하고, 변경과 컴파일에 얼마 걸리지 않는다면, 이는 전혀 문제될 게 없다. 근데, 앞으로 우리가 건드리게 될 것들이 그렇게 쉬운 문제들일까?? 버튼 한두개 만들고, 아이콘 한두개 띄우고...??

결국, 이런 테스트의 대상이 많아지면 많아질수록, 저런 식의 테스트는 비효율 그 자체가 되어버리고, 심지어는 리팩토링을 하거나 기능을 추가하는 과정에서 기존에 됐던 것이 안되거나 하는 일이 일어나도 전혀 인지하지 못하는 일이 생긴다.

테스트코드의 마법

describe('clipReducer/setClipStopTime', async (assert) => {
  const stopTime = 5;
  const clipState = {
    startTime: 2,
    stopTime: Infinity,
  };
  assert({
    given: 'clip stop time',
    should: 'set clip stop time in state',
    actual: clipReducer(clipState, setClipStopTime(stopTime)),
    expected: { ...clipState, stopTime },
  });
});

이 코드를 보면 무슨 생각이 드는가? 테스트 한번 하겠다고 너무 코드를 길게 써야하는 것 아닌가?? 싶을 수도 있겠다. 근데 그게 바로 요점이다. 이 테스트 코드는 일종의 명세서같은 개념이다. 이렇게 문서화되어있는 대로 코드를 동작시키겠다는 일종의 증명이고, 그것이 있는 한, 내가 제대로 테스트했는지에 대한 염려 자체를 할 필요가 없어진다.

결국 테스트 코드를 쓰는데 얼마나 오래걸렸는지가 중요한 것이 아니다. 뭔가 잘못되었을때, 그것을 디버깅하기 위해 얼마나 걸리는지가 중요한 것이지. 위 코드가 제대로 동작하지 않으면, 그 테스트 자체가 훌륭한 버그 리포트가 된다. 테스트 코드를 살펴보는 것만으로 어디가 문제인지를 알 수 있게 된다.

TDD는 더 나은 코드 작성법을 알려준다

유닛테스트를 작성할 때 중요한 것은, 테스트하고자 하는 부분 이외의 것들과는 완전히 독립적이어야한다는 점이다. 예를 들어, 상태 관리에 대해 테스트를 하고자 한다면 별도로 스크린을 띄우거나 데이터베이스에 어떤 동작을 하지 않고 테스트를 할 수 있어야 한다. UI를 테스트한다면 브라우저에 페이지를 로딩하거나 네트워크를 건들지 않고도 테스트할 수 있어야 한다.

TDD는 가능한한 UI 컴포넌트들을 작게 유지함으로써 훨씬 간편해진다는 점을 알려준다. UI와 비즈니스 로직, 사이드이펙트들을 구분지어 생각하자. 이는 React 등의 라이브러리를 사용할 때도, 디스플레이를 담당하는 컴포넌트와 컨테이너 컴포넌트들을 구분지어 사용하기 쉽게 만들어준다.

모든 소프트웨어 개발은 **구성(Composition)**이다. 커다란 문제들을 작고 해결이 쉬운 많은 문제들로 분해하고, 그 문제들에 대한 해결책들을 만들어 애플리케이션을 만들어간다.

유닛테스트에서의 모킹은 이렇게 구성된 애플리케이션의 구성이 실제로 그렇게 범접 못할 정도로 긴밀하게 뭉쳐져있지 않다는 것을 알려주며, 어떻게 하면 그 '분리점'을 발견해낼 수 있는지를 알려준다.

TDD에 대한 5가지 오해

  1. TDD는 시간 낭비다. 경영팀에서 절대 허용해줄 리가 없다.

  2. 디자인을 알기 전에는 테스트를 작성할 수 없다 & 코드를 실행시켜보기 전까진 그 디자인을 알 수 없다.

  3. 코드를 실행하기 전에 모든 테스트를 작성해야만 한다.

  4. Red, Green 이후 항상 리팩토링한다.

  5. 모든 것에 유닛테스트가 필요하다.

모든 유닛테스트가 갖춰야 하는 5가지 질문

  1. 무엇을 테스트하는 중인가?(모듈 / 함수 / 클래스 / 뭐든)
  2. 그것이 무엇을 해야 하는가? (설명: description)
  3. 실제로 나온 결과가 무엇인가?
  4. 예상했던 결과는 무엇인가?
  5. 어떻게 '실패'를 만들어 낼 수 있는가?