Test-driven development (TDD) 는 매우 짧은 개발 서클의 반복에 의존하는 소프트웨어 개발 프로세스 이다. 우선 개발자는 요구되는 새로운 기능에 대한 자동화된 테스트 케이스를 작성하고, 해당 테스트를 통과하는 가장 짧은 코드를 작성한다. 그리고나서, 표준에 맞게 새로운 코드를 리팩터(refactor) 한다.
TDD는 1999년도 애자일 기반 방법론이 자리 잡을 때 처음 소개가 되었다. 이 그룹에서는 실제 프로그래밍 전에 테스트 코드를 먼저 작성하면 어떨까 라고 생각하고 시도를 해 보았는데 퀄리티가 꽤 인상적이라 애자일 방법론에 TDD가 도입되기 시작했다. 즉, TDD를 좀더 알기쉽게 해석하자면 '테스트 먼저 개발'로 바꾸어 이해할 필요가 있다.
1. Add a test
: 테스트 주도형 개발에선, 새로운 기능을 추가하는 것은 우선 테스트를 작성하는 것 에서부터 시작한다. 테스트를 작성하기 위해선, 개발자는 해당 기능의 요구사항과 명세를 분명히 이해하고 있어야 한다. 이는 사용자 케이스와 사용자 스토리 등으로 이해할 수 있으며, 테스팅 프레임워크가 소프트웨어 환경에 적합한가를 따지기도 전에 테스트를 작성할 수 있도록 해준다. 이는 또한 기존 테스트의 변형일 수도 있다. 이는 코드를 작성한 뒤 유닛테스트를 하는것과는 다른 특징이다. 즉, TDD는 개발자가 코드를 작성하기 전에 보다 요구사항에 있어 집중할 수 있도록 도와주는데, 이는 작지만 중요한 차이를 만들어준다. 즉, 기존의 프로세스는 코드를 작성하기 전에 '디자인'에 충분한 시간을 투자하면서 고려하지 못하지만, TDD의 경우 보다 정확한 프로그래밍 목적을 '디자인' 단계에서 반드시 미리 정의해야만 하고, 또 무엇을 테스트해야 할지 정의해야만 한다.2. Run all tests and see if the new one fails
: 이것은 test harness 가 잘 작동하도록 만들어준다. 즉, 새로운 테스트 코드는 새로운 동작코드를 항상 요구하게 되기 때문에 새로운 동작코드를 포함하지 않아서 실수할 수 있는 확률을 원천적으로 차단한다. 이 단계에서 테스트를 스스로 테스트를 수행하는데, 새로운 테스트가 항상 잘 통과되는 가능성을 배재함으로써 새로 추가된 테스트에서 잘못된 점을 잡아낼 수 있도록 한다. 또한 새로운 테스트는 예상되는 이유에 대해 반드시 실패해야 한다. 이를통해 개발자는 유닛 테스트가 제약조건에 의해 (즉, 의도된 케이스에 대해서만) 적절히 테스팅 되었다는 자신감을 얻게된다.
3. Write some code
: 이 다음 순서는 위에서 작성한 테스트를 통과하는 코드를 작성한 일 이다. 이 단계에서 작성된 코드는 완전하지 않을 것 이다. 물론 테스트를 통과하긴 하겟지만, 이 코드는 차후에 더 향상될 가능성을 지니고있다.
4. Run tests
: 이제 모든 테스트들이 통과할 것 이다. 이제 개발자는 새로운 코드가 테스트의 요구조건에 부합하며 기존의 작성했던 기능에 대해 충돌이 일어나지 않는다는 사실에 자신감을 가질 수 있을 것 이다. 만일 그렇지 않다면 그렇게 되도록 끊임없이 코드를 수정해야 한다.
5. Refactor code
: 점점 커저가는 코드는 TDD 개발기간 동안에 정기적으로 clean up 되어야 한다. 새롭게 추가한 코드는 원래의 테스트를 통과하는 목적에서 출발하여 논리적으로 제자리에 올 수 있도록 개선되어야 한다. 중복을 제거하고, 각각의 네이밍이 목적을 분명히 할 수 있도록 수정해야 한다. 기능이 추가되면, 메소드 몸체는 좀더 커질 수 있고, 다른 오브젝트도 또한 몸집이 커질 수 있다. 이것들을 좀더 쪼개어 가독성과 유지보수에 좀더 이득을 취할 수 있으며, 상속 계층구조를 더 논리적으로 변환시키기 위해 재정렬 시킬 수 있다. 이러한 작업들은 소프트웨어 라이프사이클에 있어 굉장히 큰 가치를 제공할 것 이다. 클린 코드를 작성하기 위한 리펙토링 가이드라인이 존재한다. (Beck, Kent (1999). XP Explained, 1st Edition. Addison-Wesley Professional. p. 57 , Ottinger and Langr, Tim and Jeff. "Simple Design". Retrieved5 July 2013. ) . 각 리펙토링 단계마다 테스트 케이스가 반복적으로 수행되기 떄문에, 개발자는 해당 프로세스가 기존의 다른 기능들을 변경하지 않는다는 확신을 가실 수 있게된다.
- Repeat
: 또다른 기능을 추기하기 위한 새로운 테스트를 시작하면서, 위의 사이클은 다시한번 반복된다. 해당 테스트 사이클은 적당히 작아야 하며, 새로운 코드가 새로운 테스트를 빠르게 통과하지 못했을 경우 다시 이전 단계로 회귀할 것 이다. 즉, 이와같은 지속적 통합은 언제나 되돌아갈 수 있는 회귀점을 제공해준다. 외부 라이브러리를 사용할 경우, 특별히 외부 라이브러리가 버그가 있을것 이라는 의심이 들지 않는경우, 해당 라이브러리를 테스트하는데에 너무 큰 부분을 차지하지 않는것이 좋다.
TDD의 장점
TDD의 가장 큰 장점은 높은 퀄리티의 소프트웨어를 보장한다는 것이다. 에러나 버그가 없으며, 추가적인 요구사항이 있을 때 손쉽게 그 요구사항을 반영해줄 수 있으며, 누가 그 코드를 봐도 손쉽게 이해하고 수정할 수 있어야 한다는 것이다.
보다 튼튼한 객체지향적인 코드 생산 가능
테스트 코드를 먼저 작성한다는 것은 하나하나의 기능들에 대해서 철저히 구조화 시켜 코드를 작성한다는 것을 뜻한다. TDD는 “모든 코드”가 재사용성 기반으로 작성되어야 하기 때문에 보다 튼튼한 코드 생산이 이루어 지게 된다.
재설계 시간의 단축
앞에서 설명한 것처럼 테스트 코드를 먼저 작성하기 때문에 내가 지금 무엇을 해야 하는지 분명히 정의를 하고 시작하게 된다. 테스트 코드를 작성하면서 인터페이스나 클래스의 구조들을 많이 수정하게 된다. 만약 테스트 코드가 아니라 실제 구현 코드를 작성하면서 이 작업을 할 경우에 구현하고 있는 코드들 또한 고쳐야 하는 문제를 유발하게 된다. 실제로 초급 개발자와 중고급 개발자의 차이는 얼마만큼 예외 상황을 많이 알고 있느냐의 차이로 나누어 지기도 한다. 여기서 만약 먼저 테스트 시나리오들을 정의하게 되면 그만큼 필요한 예외 상황들을 먼저 선임들과 상의할 수 있다는 것이고, 초급 개발자들도 예외 상황들과 테스트 되어야 하는 가능성들을 먼저 조사하게 되는 것이다.
디버깅 시간의 단축
이것은 통합 테스팅이 아닌 유닛 테스팅을 하는 이점이기도 하다. 아래서 보다 정확한 논리를 설명할 것이지만 먼저 간단하게 설명하자면 우리는 각각의 모듈 별로 테스트를 자동화할 수 있는 코드가 없다면 특정 버그를 찾기 위해서 모든 레벨(레이어)의 코드들을 살펴봐야 한다. 예를 들어 사용자의 데이터가 잘못 나온다면 DB의 문제인지, 비즈니스 레이어의 문제인지 UI 단의 문제인지 실제 모든 레이어들을 전부다 디버깅할 필요 없이 자동화 된 유닛테스팅 결과로 우리는 손 쉽게 찾아 낼 수 있다는 것이다.
테스트 문서의 대체가능
주로 S.I 프로젝트를 진행하다 보면 어떤 요소들이 테스트 되었는지 테스트 정의서를 만들고는 한다. 이것은 단순 통합테스트 문서에 지나지 않는다. 즉, 내부적인 하나하나의 로직들이 어떻게 테스트 되었는지 제공할 수 없다. 하지만 TDD를 하게 될 경우에 테스팅을 자동화 시킴과 동시에 보다 정확한 테스트 근거를 산출할 수 있다.
추가 구현의 용이함
개발 뒤에 어떤 기능을 추가할 때 가장 우리를 번거롭게 하는 것이 이 기능이 기존의 코드들에 얼만큼 영향을 미치게 될지 모르기 때문에 다시 모든 코드들을 테스트 해야 되는 것이 큰 곤욕이다. 역시나 바로 뒷 부분에 살펴볼 내용이지만 유닛 테스트로 자동화 될 경우에 우리는 수동으로 모든 테스트를 다시 진행해야 되는 시간적인 낭비를 줄일 수 있다.