Test Driven Development (TDD)

TDD(Test-Driven-Development) 는 애자일을 실천하기 위한 소프트웨어 개발방법론 중 하나이다. Test First Development(TFD) 와 Refactoring이 결합된 형태라고도 한다.

TDD 과정

보통은 개발할 때 설계 -> 코드 개발 -> 테스트 와 같은 과정을 거치게 된다. 하지만 TDD는 기존 방법과는 다르게, 테스트케이스를 먼저 작성한 이후에 실제 코드를 개발하는 리팩토링 절차를 밟게된다.

normal
tdd

위 단계를 조금 더 구체적으로 설명하자면 아래와 같다.

  1. 테스트 코드를 먼저 작성함으로써 테스트 코드가 개발을 주도하도록 한다.
    • 테스트 코드가 개발을 주도하기 위해서는 반드시 실패를 포함하는 테스트 코드의 작성이 앞서야 한다.
  2. 앞서 작성된 테스트 코드를 통과할 수 있는 ‘최소한의 구현 코드’를 작성한다.
    • 최소한의 구현 코드는 개선될 수 있는 많은 여지를 퐇마하는 코드이다. 단지 테스트만 패스하면 되도록 작성한다.
  3. 최소한의 구현 코드를 리팩토링 단계에서 개선한다.
  4. 테스트 코드 작성, 최소한의 구현 코드 작성, 구현 코드에 대한 리팩토링 순으로 짧은 주기를 반복하며 점증적으로 개발한다.

이 때 테스트케이스는 자동화가 되어 생산성을 높일 수 있어야 한다. (JUnit 같은 툴을 이용)

장점

  1. 당연하게도 결함이 줄어든다. 목표로 하는 테스트케이스를 먼저 작성하고 테스트를 통과하게 코드를 작성하기 때문에 결함을 줄일 수 있다.
  2. 개발을 할 때 작은 단계별로 나누어 코드를 작성할 수 있도록 한다. 새로운 기능을 위한 코드를 작성한다고 가정했을 때, TDD로 개발을 수행했다면 기존 코드는 이미 Test로 검증이 되어있기 때문에 추가된 코드로 범위를 한정지어 결함을 찾기 더 쉬워진다.
  3. TDD는 코드 수정에 대해 불안감을 떨치고 확신을 얻을 수 있게 해주는 강점을 갖게해준다고 할 수 있다.
  4. 또한, 테스트 코드를 먼저 작성하는 것은 목표를 미리 구상하는 작업이라고 할 수 있다. 클래스를 만들고 어떻게 동작할지를 먼저 정의하는 구현보다는 객체가 어떤 행동을 해야하는지에 대한 인터페이스에 집중하게 하여 객체가 어떤 방식으로 기능을 수행하는지 보다는 객체가 무슨 역할을 가져야 하는지 그리고 이러한 역할을 위해 무엇을 노출해야하는지 등에 집중할 수 있다.
  5. 4번 과정에 집중하면 코드의 복잡도가 낮아지게 된다. 코드의 복잡도가 낮아지는 것은 결국 읽기 쉬운 클린 코드를 짤 수 있게 된다는 뜻이며, 이는 곧 유지보수 비용의 감소로 이어질 수 있다.

Unit Test을 작성하는 행위는 검증보다는 설계와 문서화 행위라고 할 수 있다. 단위 테스트를 작성함으로써 기능 검증과 관련된 많은 피드백 루프를 최소화할 수 있다. - Bob martin

단점?

  1. 기존 개발 프로세스에 비해 테스트케이스 설계가 추가되므로 생산비용이 증가된다.
    • 이것은 한정적으로 맞는 얘기인데, 단기적인 프로젝트를 완성하고 더 이상 그 프로젝트를 볼 일이 없거나 성과를 우선으로 내야한다면 테스트코드를 설계하고 작성하는 시간이 추가되기 때문에 손해일 수 있다.
    • 하지만, 장기적인 product를 개발한다는 관점에서 보면 이는 틀린 말일 수 있다. 테스트 코드를 작성하는데에 초기 비용이 늘어나는 것은 분명한 사실이지만, 장기적으로 보았을 때 더 높은 품질의 코드와 결함을 고치는 시간이 감소됨이 따라 오히려 생산성이 증가한다. (옛날에 작성해둔 버그를 찾는 시간과 이를 고치는 시간이 대부분 감소한다고 볼 수 있다.)
  2. 어렵다
    • 이제껏 개발하던 방식을 바꿔야하기 때문에 당연히 어려울 수 있다.

Unit Test

TDD에서 결국 자동화된 테스트케이스 중 상당 부분은 Unit Test일 것이다.

Unit Test는 Unit 을 테스트하는 방법으로, Unit이 의미하는 것은 테스트가 가능한 가장 작은 단위의 소프트웨어 컴포넌트를 의미하며 이것들은 대체로 단일 기능을 수행한다. Unit은 작은 규모여서 설계, 실행, 기록과 테스트결과의 분석이 쉬워야 한다. 좋은 Unit Test를 작성하기 위해서는 Unit이 의미하는 컴포넌트의 구조 또한 중요하다는 뜻이다.

개발일정의 단축을 위해(또는 귀찮다는 이유로..) 유닛테스트를 생략한다면 시스템 테스트나 통합 테스트 나아가 빌드 이후에 베타 테스트를 진행할 때 많은 버그가 일어날 가능성이 매우 높고 이 단계에서 결함을 발견하고 수정하는 비용은 매우 높다.

defect_cost_graph

결국 유닛테스트의 특징과 장점, 해야하는 이유 등은 TDD와 동일하다고 생각할 수 있다. (적어도 나한테는 그렇게 느껴진다.)

Best Practices

단위 테스트를 설계하고 작성하는데에 있어 Best Practices를 살펴보자.

1. 단위 테스트는 신뢰할 수 있어야 한다.

테스트는 코드가 잘못된 경우에만 실패해야 한다. 그렇지 않으면 테스트 결과를 신뢰할 수 없다.

2. 단위 테스트는 유지가 가능해야하며 가독성이 좋아야한다.

production 코드가 변경되면 테스트도 업데이트 되어야 한다. 그러므로, 테스트 코드 또한 읽고 이해하기 쉬워야한다. 항상 정리하고 이름을 명료하고 가독성 좋게 붙일 것.

3. 단위 테스트는 단일 유스케이스에 대해서만 입증해야 한다.

좋은 테스트는 단일 유즈케이스에 대한 한 가지에 대해서만 입증한다. 이렇게 되면, 테스트 코드가 단순해지고 이해하기 쉬워지며 이는 유지보수나 디버깅을 하기에 용이하게 만들어준다. 테스트가 한 가지 이상의 기능을 입증하게 되면 테스트 코드가 복잡해지며 유지보수에 많은 시간이 걸린다.

4. 단위 테스트는 고립되어야 한다.

테스트는 순서나 서로에게 영향을 주는 일이 없이 어떤 곳에서든 실행할 수 있어야 한다. 가능하다면, 글로벌하거나 외부의 환경 요소에 대해 의존성을 갖지 않게 하는것이 좋다. 의존성이 있는 테스트는 불안정하며 실행하기 어렵고 유지보수나 디버그가 어렵게 한다.

5. 단위 테스트는 자동화되어야 한다.

단위테스트는 매일, 매 시간 또는 CI/CD 환경에서 매번 일어나기 때문에 자동화된 프로세스에서 돌아가도록 해야한다. 자동화된 테스트의 결과는 쉽게 접근 가능하며 모든 팀원이 리뷰해야 한다.

6. 테스트 단계별 분배를 잘 해야한다.

테스트 자원의 이상적인 분배를 묘사하기 위해 흔히 사용되는 Testing Pyramid Model을 보면, Pyramid의 상단으로 갈 수록 테스트를 빌드하기 복잡하고 깨지기 쉬우며 실행 속도도 느리고 디버그도 느리다. 그러므로, 자동화된 단위 테스트를 통해 충분한 테스트를 거치고 상위 단계로 올라가는 것이 좋다. testing_pyramid_model

7. 단위 테스트는 조직 내에서 정립된 관행을 가지고 실행되어야 한다.

어떤 난이도의 테스트도 잘 이끌며 확장성이고 유연한 단위 테스트 프로세스를 얻기 위해서는 적절한 테스트 방법을 정리할 필요가 있다. production 코드를 작성하듯 테스트 코드를 작성하라는 뜻이다. 테스트 코드 역시 팀원들과 함께 좋은 테스트 코드인가에 대해 의논할 필요가 있다. 이를 위해 조직 내에 테스트 코드의 작성 규칙을 정립하고 이 규칙에 맞춰 테스트를 작성하고 실행하는 노력이 필요하다.

결론

TDD 역시 탄탄하고 유연한 설계가 뒷받침 되어야 훌륭한 테스트케이스 설계와 테스트 코드가 나올 수 있다고 생각한다. 모든 개발 조직이 TDD를 사용해야 한다는 것은 아니지만, 더 효율적인 방법을 위해서 배우는 시간을 투자하는 것이 아깝고 그 시간에 하나라도 더 찍어내는게 맞다고 생각이 든다면 이미 도태되는 과정이라고 생각된다.

다음 포스팅은 JUnit을 이용한 Java, Spring 환경에서 단위테스트를 작성하는 방법에 대해 정리 할 예정.

참고