최근 수정 시각 : 2025-04-18 07:18:19

테스트 주도 개발


파일:다른 뜻 아이콘.svg  
#!if 넘어옴1 != null
'''TDD'''{{{#!if 넘어옴2 != null
, ''''''}}}{{{#!if 넘어옴3 != null
, ''''''}}}{{{#!if 넘어옴4 != null
, ''''''}}}{{{#!if 넘어옴5 != null
, ''''''}}}{{{#!if 넘어옴6 != null
, ''''''}}}{{{#!if 넘어옴7 != null
, ''''''}}}{{{#!if 넘어옴8 != null
, ''''''}}}{{{#!if 넘어옴9 != null
, ''''''}}}{{{#!if 넘어옴10 != null
, ''''''}}}은(는) 여기로 연결됩니다. 
#!if 설명 == null && 리스트 == null
{{{#!if 설명1 == null
다른 뜻에 대한 내용은 아래 문서를}}}{{{#!if 설명1 != null
{{{#!html 풀 메탈 패닉의 TDD-1}}}에 대한 내용은 [[투아하 데 다난(풀 메탈 패닉!)]] 문서{{{#!if (문단1 == null) == (앵커1 == null)
를}}}{{{#!if 문단1 != null & 앵커1 == null
의 [[투아하 데 다난(풀 메탈 패닉!)#s-|]]번 문단을}}}{{{#!if 문단1 == null & 앵커1 != null
의 [[투아하 데 다난(풀 메탈 패닉!)#|]] 부분을}}}}}}{{{#!if 설명2 != null
, {{{#!html }}}에 대한 내용은 [[]] 문서{{{#!if (문단2 == null) == (앵커2 == null)
를}}}{{{#!if 문단2 != null & 앵커2 == null
의 [[#s-|]]번 문단을}}}{{{#!if 문단2 == null & 앵커2 != null
의 [[#|]] 부분을}}}}}}{{{#!if 설명3 != null
, {{{#!html }}}에 대한 내용은 [[]] 문서{{{#!if (문단3 == null) == (앵커3 == null)
를}}}{{{#!if 문단3 != null & 앵커3 == null
의 [[#s-|]]번 문단을}}}{{{#!if 문단3 == null & 앵커3 != null
의 [[#|]] 부분을}}}}}}{{{#!if 설명4 != null
, {{{#!html }}}에 대한 내용은 [[]] 문서{{{#!if (문단4 == null) == (앵커4 == null)
를}}}{{{#!if 문단4 != null & 앵커4 == null
의 [[#s-|]]번 문단을}}}{{{#!if 문단4 == null & 앵커4 != null
의 [[#|]] 부분을}}}}}}{{{#!if 설명5 != null
, {{{#!html }}}에 대한 내용은 [[]] 문서{{{#!if (문단5 == null) == (앵커5 == null)
를}}}{{{#!if 문단5 != null & 앵커5 == null
의 [[#s-|]]번 문단을}}}{{{#!if 문단5 == null & 앵커5 != null
의 [[#|]] 부분을}}}}}}{{{#!if 설명6 != null
, {{{#!html }}}에 대한 내용은 [[]] 문서{{{#!if (문단6 == null) == (앵커6 == null)
를}}}{{{#!if 문단6 != null & 앵커6 == null
의 [[#s-|]]번 문단을}}}{{{#!if 문단6 == null & 앵커6 != null
의 [[#|]] 부분을}}}}}}{{{#!if 설명7 != null
, {{{#!html }}}에 대한 내용은 [[]] 문서{{{#!if (문단7 == null) == (앵커7 == null)
를}}}{{{#!if 문단7 != null & 앵커7 == null
의 [[#s-|]]번 문단을}}}{{{#!if 문단7 == null & 앵커7 != null
의 [[#|]] 부분을}}}}}}{{{#!if 설명8 != null
, {{{#!html }}}에 대한 내용은 [[]] 문서{{{#!if (문단8 == null) == (앵커8 == null)
를}}}{{{#!if 문단8 != null & 앵커8 == null
의 [[#s-|]]번 문단을}}}{{{#!if 문단8 == null & 앵커8 != null
의 [[#|]] 부분을}}}}}}{{{#!if 설명9 != null
, {{{#!html }}}에 대한 내용은 [[]] 문서{{{#!if (문단9 == null) == (앵커9 == null)
를}}}{{{#!if 문단9 != null & 앵커9 == null
의 [[#s-|]]번 문단을}}}{{{#!if 문단9 == null & 앵커9 != null
의 [[#|]] 부분을}}}}}}{{{#!if 설명10 != null
, {{{#!html }}}에 대한 내용은 [[]] 문서{{{#!if (문단10 == null) == (앵커10 == null)
를}}}{{{#!if 문단10 != null & 앵커10 == null
의 [[#s-|]]번 문단을}}}{{{#!if 문단10 == null & 앵커10 != null
의 [[#|]] 부분을}}}}}}
#!if 설명 == null
{{{#!if 리스트 != null
다른 뜻에 대한 내용은 아래 문서를}}} 참고하십시오.

#!if 리스트 != null
{{{#!if 문서명1 != null
 * {{{#!if 설명1 != null
풀 메탈 패닉의 TDD-1: }}}[[투아하 데 다난(풀 메탈 패닉!)]] {{{#!if 문단1 != null & 앵커1 == null
문서의 [[투아하 데 다난(풀 메탈 패닉!)#s-|]]번 문단}}}{{{#!if 문단1 == null & 앵커1 != null
문서의 [[투아하 데 다난(풀 메탈 패닉!)#|]] 부분}}}}}}{{{#!if 문서명2 != null
 * {{{#!if 설명2 != null
: }}}[[]] {{{#!if 문단2 != null & 앵커2 == null
문서의 [[#s-|]]번 문단}}}{{{#!if 문단2 == null & 앵커2 != null
문서의 [[#|]] 부분}}}}}}{{{#!if 문서명3 != null
 * {{{#!if 설명3 != null
: }}}[[]] {{{#!if 문단3 != null & 앵커3 == null
문서의 [[#s-|]]번 문단}}}{{{#!if 문단3 == null & 앵커3 != null
문서의 [[#|]] 부분}}}}}}{{{#!if 문서명4 != null
 * {{{#!if 설명4 != null
: }}}[[]] {{{#!if 문단4 != null & 앵커4 == null
문서의 [[#s-|]]번 문단}}}{{{#!if 문단4 == null & 앵커4 != null
문서의 [[#|]] 부분}}}}}}{{{#!if 문서명5 != null
 * {{{#!if 설명5 != null
: }}}[[]] {{{#!if 문단5 != null & 앵커5 == null
문서의 [[#s-|]]번 문단}}}{{{#!if 문단5 == null & 앵커5 != null
문서의 [[#|]] 부분}}}}}}{{{#!if 문서명6 != null
 * {{{#!if 설명6 != null
: }}}[[]] {{{#!if 문단6 != null & 앵커6 == null
문서의 [[#s-|]]번 문단}}}{{{#!if 문단6 == null & 앵커6 != null
문서의 [[#|]] 부분}}}}}}{{{#!if 문서명7 != null
 * {{{#!if 설명7 != null
: }}}[[]] {{{#!if 문단7 != null & 앵커7 == null
문서의 [[#s-|]]번 문단}}}{{{#!if 문단7 == null & 앵커7 != null
문서의 [[#|]] 부분}}}}}}{{{#!if 문서명8 != null
 * {{{#!if 설명8 != null
: }}}[[]] {{{#!if 문단8 != null & 앵커8 == null
문서의 [[#s-|]]번 문단}}}{{{#!if 문단8 == null & 앵커8 != null
문서의 [[#|]] 부분}}}}}}{{{#!if 문서명9 != null
 * {{{#!if 설명9 != null
: }}}[[]] {{{#!if 문단9 != null & 앵커9 == null
문서의 [[#s-|]]번 문단}}}{{{#!if 문단9 == null & 앵커9 != null
문서의 [[#|]] 부분}}}}}}{{{#!if 문서명10 != null
 * {{{#!if 설명10 != null
: }}}[[]] {{{#!if 문단10 != null & 앵커10 == null
문서의 [[#s-|]]번 문단}}}{{{#!if 문단10 == null & 앵커10 != null
문서의 [[#|]] 부분}}}}}}

파일:tdd.webp
1. 개요2. 상세3. 분류
3.1. 단위 테스트
3.1.1. stub, mock
3.2. 통합 테스트3.3. 인수 테스트
4. 장점5. 한계 및 비판6. 테스팅 프레임워크7. 코드 커버리지8. CI

1. 개요

Test Driven Development, TDD

소프트웨어를 개발 또는 설계할 때, 요구사항에서 유도 가능한 선제적 테스트를 도출하여 작성한 후, 실제적인 구현 착수보다 우선하는 소프트웨어 개발 방법론.

2. 상세

전통적인 공학론적 개발 프로세스는 사전에 철저히 검증된 계획 하에 장기간에 걸쳐 많은 인원과 비용을 투입하여 목표를 완수하는 방식을 따른다. 소프트웨어 개발 역시 초기에는 이러한 프로세스에 따라 폭포수 모델 등을 따랐으나, 구현하게 될 소프트웨어의 규모가 커지고 복잡해짐에 따라 소프트웨어 위기(Software crisis)라는 문제에 봉착하게 되고, 소프트웨어 공학에서는 소프트웨어 개발 과정이 다른 공학적 방식과 큰 차이가 있음을 인지하게 된다.

소프트웨어는 유동적이고 예측하기 어렵다. 또한 소프트웨어가 발전하면서 점차 확장가능성, 개방적 구조를 요구하게 되었다. 따라서 구체적으로 개발이 언제 완료될 것인지 예측하는 것이 매우 어려울 뿐더러, 인원과 비용을 늘린다고 해서 개발 시간의 절감이나 질적인 성과를 보장할 수 없다. 소프트웨어의 규모가 커지고 복잡해질수록 기존의 폭포수 모델을 적용했을 때 다음과 같이 다양한 문제점이 발견되었다.
  1. 개발에 적용할 수 있을 수준의 구체적인 요구사항을 작성하는 것이 매우 어려움. 불가능함.
  2. 규모가 커질수록 설계에 요구되는 시간과 비용이 기하급수적으로 증대됨.
  3. 실제로 개발에 들어가고나서 정해진 요구사항이 변경되거나, 다양한 문제점이 발견됨.
  4. 위와 같은 문제로 인해 작업 난이도 및 개발일정을 예측하는 것이 어려움.

기존의 계획주도 개발방식에서는 세운 계획대로 개발이 흘러가지 않으며, 그것을 보완하기 위해 언제나 개발이 지연되었고, 개발자에게 주어지는 스트레스와 과중한 업무, 그 결과 떨어지는 생산성과 상품성 등에 직면하여 소프트웨어 공학자들은 전통적인 개발 프로세스에 큰 의문을 제기하였다. 이에 완전한 설계를 지향하는 폭포수 모델을 폐기하고, 초기의 설계 비용을 줄이고자 작은 규모의 소프트웨어를 완성한 다음 그것을 점차 보완해가며 복잡한 소프트웨어를 완성하는 나선형 모델(Spiral model)을 도입하는 등 산업에서는 보다 경험적 프로세스 제어 모델로 이행하게 된다.

애자일 프로그래밍(Agile programming) 방식은 계획과 문서에 의존하는 기존의 방식을 부정한다. 미래에 대한 예측을 차단하고 지속적인 프로토타입의 완성을 반복하여 그때그때 소단위 요구사항을 추가하고 기존의 문제점을 해결하여 점차 큰 규모의 소프트웨어를 완성하는 개발 방식이다. 익스트림 프로그래밍(eXtream Programming, XP)은 대표적인 애자일 프로그래밍 개발방법론 중 하나로, 고객이 원하는 소프트웨어를 빠른 시간 내에(약 2주) 프로토타입의 형태로 전달하고 이를 통해 고객이 원하는 소프트웨어를 이끌어내며, 수시로 발생하는 요구사항에 대처하는 것을 목표로 한다.

다른 애자일 방법론과 구별되는 XP의 특징은 테스팅이다. 구현과 테스트를 하나의 쌍으로 취급하여, 실제 구현과 동시에 테스트 코드를 작성하도록 하며, 이것에 기반한 프로젝트 발전 과정은 애자일 방법론의 기본 개념인 "반복적으로 프로토 타입을 고객에 전달함으로써 고객의 요구사항 변화에 민첩하게 대응한다"를 실천하는데에 큰 도움을 줄 수 있다. 왜냐하면 매번 프로토 타입을 고객에 전달함에 있어서 프로토 타입 자체로써 버그가 상대적으로 적은 완벽에 가까운 데모를 경험하게 해줄 수 있기 때문이다.

테스트 주도 개발(Test Driven Development, TDD)은 익스트림 프로그래밍 개발방법론의 실천 방안 중 하나이다. 개발이 이루어진 다음 그것이 계획대로 잘 완성되었는지 테스트 케이스를 작성하고 테스트하는 타 방식과는 달리, 테스트 케이스를 먼저 작성한 다음 테스트 케이스에 맞추어 실제 개발 단계로 이행하는 개발방법론을 말한다. 묵시적으로 잠재된 상황을 가정하지 않고 테스트 케이스만을 완벽하게 수행하는 것을 목표로 하기 때문에 매우 빠르게 목표를 완료할 수 있다. 한편, TDD 자체가 하나의 테스트가 완전하지 않다는 것을 가정하고 있기 때문에 1차 테스트를 완료한 다음에 새로운 테스트 케이스를 확장해서 작성하고 그것을 통과하기 위한 개발에 들어가는 과정을 끊임없이 반복하여 큰 규모의 프로젝트를 완성해가는 것이다.

3. 분류

3.1. 단위 테스트

unit test

목적 소프트웨어를 이루는 개별 단위 수준에서 독립적으로 진행하는 테스트의 유형. 여기서 단위(unit)가 무엇인지는 언어나 프로젝트에 따라 정의가 달라질 수 있으며, 반드시 분리 가능한 최소 단위일 것을 요구하지는 않는다. 예를 들어 너무 단순하고 자잘한 함수 하나하나까지 테스트하는 것은 되려 생산성을 저하시키는 overtesting일 것이다. 일반적으로 클래스 등 적당한 크기로 분리 가능한 구조를 테스트 단위로 하는 편.

테스트하고자 하는 대상 코드의 구현을 개발자가 명확히 파악 및 인지하고 있다는 조건 하에서 작성되기에 화이트박스 테스트라고도 불린다.

3.1.1. stub, mock

단위 테스트는 개발자가 작성한 개별(단위의) 코드 그 자체의 동작만을 검증하기 위한 것이 목적이기 때문에, 올바른 단위 테스트를 작성하려면 해당 코드를 제외한 외부 의존성이나 관계는 철저히 배제해야 한다. 이 때문에 코드가 네트워크 응답이나 데이터베이스 등 외부 자원을 필요로 한다면 이를 대신해 흉내낼 일종의 '가짜 코드'가 있어야 하는데, 이를 stub이라고 한다. stub에 더해, 테스트하고자 하는 내부 코드에 의해 올바른 값으로 호출되거나 몇 차례 호출되었는지를 비파괴적으로 확인할 수 있는 기능을 갖춘 코드를 mock이라고 한다. 도메인에 따라 fake 또는 spy, fixture, test double 등으로도 불리며 사람마다 용어의 미묘한 정의가 갈리는 일이 많은 편.#

단위 테스트를 작성하려면, 일단 테스트를 작성한 뒤 컴파일 에러가 일어나지 않도록 테스트에서 쓰이는 클래스와 그 메소드의 스텁을 만들어 둔다. 테스트가 실패하는 것을 확인하고(= 컴파일 에러는 없는 것을 확인하고) 클래스를 구현한다. 클래스에서 필요한 다른 클래스가 필요하면 일단 스텁으로 지금 작성중인 클래스의 테스트를 통과하게만 해 두고, 나중에 그 클래스의 테스트 케이스를 작성한 후 구현한다. 이런 식으로 프로젝트 전체를 완성해 나간다.

3.2. 통합 테스트

integration test

단위 테스트로 검증된 개별 요소들을 결합해 하나의 큰 동작을 수행하는 데 문제가 없는지를 검증하기 위한 테스트. 단위 테스트는 개별 요소가 의도한 대로 동작한다는 사실만 보여줄 뿐 정작 실제 코드에 도입해 사용할 때면 예기치 못한 문제가 드러날 수 있기 때문에 필요하다.

계층적으로 단위 테스트와 인수 테스트의 중간 지점에 위치해 있으며, 필요하다면 네트워크나 데이터베이스 같은 외부 종속성을 mock 없이 사용하기도, mock 해서 사용하기도 한다.

기본적으로 개별 구성 요소들의 테스트가 모두 성공할 것이 필요조건이므로, 생명주기가 매우 긴 테스트이다.

3.3. 인수 테스트

통합 테스트 이후부터는 분야나 프로세스마다 명칭이 매우 상이하며, 기능 테스트, e2e(end-to-end) 테스트 등으로도 불린다.

프로젝트의 최종 인수 조건을 검수하기 위한 목적의 테스트로, 제품 설계시 산출되는 요구사항의 가장 원형에 가까운 동작을 기술하며, 예를 들어 '사용자가 광고를 검색한다', '사용자가 메모를 휴지통에 넣는다'와 같이 최종 제품의 사용자 관점(user story)에서 서술된 테스트이다. 테스트 시 코드의 구현에 대해 신경쓰지 않으므로, 블랙박스 테스트라고도 불린다.

전통적으로 인수 테스트는 사람(QA)의 영역으로 취급받곤 하나, 분야에 따라 코드로도 일부 기술할 수 있으며 그 경우 품질 개선에 큰 도움이 된다.

4. 장점

  • 코드의 유지보수가 용이해진다
    프로그래밍 개발에서는 처음 개발할 때보다 이미 개발한 코드의 버그를 수정하고, 최적화하고, 새 기능을 추가할 때 비용이 더 들어간다. 그런데 테스트를 작성하면 코드에 절대로 뒤떨어지지 않는 문서가 탄생하며, 다른 코드의 행위가 보증되므로 원하는 부분에만 신경을 쓸 수 있으며, 테스트하기 쉬운 코드는 자연히 품질이 높아지므로 다시 읽기도 편하다. 또한 테스트가 있으면 안심하고 코드를 리팩토링할 수 있다.
  • 빠른 피드백
    새 코드를 작성한 다음 기능에 문제는 없는지, 타입을 잘못 설정하지는 않았는지, edge case에 대한 문제는 없는지를 확인하기 위해서는 일반적으로 해당 코드가 병합되고, 실제 제품에 통합될 때까지 기다려야 한다. 반면 테스트를 작성하면 코드가 통합되기 전, 배포되기 전에도 문제 있는 기능에 대한 피드백을 받을 수 있고 이는 TDD와 애자일에서 말하는 '피드백 시간의 단축' 원칙과 정확하게 부합하는 사례이다. 개별 단위의 피드백 주기가 빨라질수록, 큰 기능의 피드백 소모 시간 역시 사전 피드백으로 단축된 시간에 비례하게 줄어들기 때문.
  • 디버깅 시간 단축
    테스트를 작성하는 시간을 포함시키고도 오히려 전체 작업 시간은 줄어든다. 흔히 코드를 작성하는 시간이 작업 시간의 전부라고 착각하기 쉬우나, 현실은 대부분의 시간이 디버깅에 투입된다. 이 때 테스팅은 디버깅을 해야 할 범위를 단위(unit) 안으로 제한함으로써 디버깅에 들어가는 노고를 크게 줄여준다. 예시로 테스트가 없었다면, 만약 완성된(integrated) 제품을 실제로 가동시켰을 때 버그가 포착된 경우, 어떤 경로(trace)로 버그가 발생하는지, 즉 어떤 코드가 근본적인 버그의 원인(cause)인지를 거슬러 올라가는 작업부터 시작해야 한다. 반대로 테스트를 사용한다면 #6번, #17번, #44번 테스트가 결과값이 예상과 달라 실패한 것을 보고 여기서부터 시작하여 디버깅 시간을 압도적으로 단축할 수 있다.
  • 문서화(test as a documentation)
    테스트를 작성하는 것 자체가 훌륭한 문서화이기도 하다. 소스 코드 중간중간의 주석은 왜 코드가 이렇게 짜여져 있는지를 기록한다면, 유닛 테스트는 코드가 어떻게 행동(behave)해야 하는지를 기록한다. 따라서 다른 프로그래머들이 쓴 (또는 과거의 자신이 쓴) 코드를 파악하고 프로그램을 수정, 확장하는데 시간과 비용이 크게 단축된다.

5. 한계 및 비판

완벽하게 준비되고 계획된 테스트 주도 개발에는 큰 단점이 없다. 그러나, 이상과 다르게 현실은 항상 이론대로만 흘러가지 않고, 테스트 주도 개발의 이론을 극단적인 형태로 실무에 적용하는 행위는 여러 한계점이 있다.

후술하다시피 테스트 주도 개발 원칙의 이념과 목적, 그리고 해결하고자 하는 근본적인 문제를 이해한 다음 실무에서는 상황에 따라 적절하게 맞추어 도입하는 것이 좋으며, 섣불리 적용하는 것은 마치 애자일 선언문만 본 다음 이를 실무에 적용하려 하는 것과 같다.
  • 인간은 예측하지 못한 실수를 사전에 떠올릴 수 없다
    현실적으로 테스트 케이스를 미리 작성하고 구현을 시작한다는 것은 이미 해당 실수가 일어날 수 있다고 명확히 인지하고 있는 상황에서나 적용 가능한 이야기다. 대표적으로 결과가 확정되어 있는 수학 라이브러리 코드 등이 이에 속한다. 일례로 루트를 계산할 때 뉴턴-랩슨 방법보다 빠른 방법을 구현하기 위해 코드를 작성한다면, 그 결과 값은 항상 기존의 정답과 같아야 한다. 그렇기 때문에 몇몇의 예외 케이스들을 테스트한다면 해당 문제를 잡고, 새 구현이 잘 동작함을 귀납적으로 검증할 수 있다.

    하지만 이같은 특수한 경우가 아닌 대부분의 프로그래밍 사례에서는 미리 작성된 테스트가 개발자의 실수를 답습하기만 한다. 즉 변명을 하기 위한 완벽한 변명거리가 된다. 기본적으로 인간의 실수는 코드를 작성할 때 인지할 수 없다. 코드를 쓰고 문제가 발생 했을 때 테스트를 작성하나, 테스트를 작성하고 코드를 작성하고 문제를 발견하나 결국 발생할 실수는 똑같이 발생하기 때문이다. 장점으로 알려진 디버깅 시간 단축은 그저 이론적인 환상에 가깝다.
  • 테스트가 모두 통과하므로 버그가 없다고 생각하는 함정에 빠질 수 있다
    녹색 불이 들어오면서 통과하는 정돈되고 멋진 전체 테스트 목록은 개발자에게 높은 쾌감과 확신을 준다. 이 때문에 꽤 잘 갖추어진 테스트 목록이 완성되면, 개발자는 섣불리 코드를 확신에 차서 배포하다가 테스트에서는 고려하지 못한 문제를 만나 버그를 만들고 당황하기도 한다.

    그러나 테스트의 존재는 1. 구현 코드에 버그가 없고, 2. 구현 코드가 고객의 비즈니스 요구사항을 완벽히 만족시킨다는 것을 보장하지 않는다. 개발자는 구현할 로직의 모든 스펙과 부작용을 완벽히 예측할 수 없고, 비즈니스 요구사항은 계속 변화한다. 어제까지 완벽했던 코드는 오늘 협력사의 업데이트로 인해 무너질 수 있다. 오늘까지 맞았던 로직은 내일 회사의 목표나 고객의 변심에 의해 틀릴 수 있다. 테스트와 테스트 주도 개발은 "이 구현 코드가 문제없이 완벽함"을 보증하는 것이 아니라 "이 구현 코드가 여기까지는 문제없음을 확인함"을 보증하는 것이다. "여기까지는 괜찮음"을 확인한 것만으로도, TDD는 제 역할을 한 것이다. 테스트를 중심으로 개발한 덕분에 개발자는 새로운 변경을 만들 때 전체 소프트웨어가 문제가 없는지를 처음으로 돌아가서 점검할 수고를 덜기 때문이다.
  • 과테스트(overtesting)
    개별 함수 하나하나, 사소한 기능 하나하나마다 테스트를 작성하는 경우로, 결과적으로는 프로젝트 진행에 아무런 도움이 안 되는 시간 낭비 행위이다. 이러한 문제가 발생하는 원인은 근본적으로 단위(unit) 설정의 실패로, 최소한의 독립성이 존재하는 기능적 덩어리가 아니라 그저 코드 한줄 한줄을 비의미적으로 쪼개서 잘못 보고 있는 것이다. 흔히 코드베이스 전체의 테스트 커버리지를 굳이 100%로 채우려고 시도하거나 위에서 설명한 대로 성공하는 테스트로 인한 착각성 만족감에 도취된 경우로, 이러한 과테스팅의 문제점은 켄트 벡 역시 경고한 것이 있다.

    특히나 이렇게 의미적 경계를 무시한 테스트가 늘어나면 추후 코드 구조를 개선할 때나 리팩토링을 할 때도 기존 테스트를 대거 수정해야 하는 등, TDD의 장점을 전혀 살리지 못하고 오히려 개발에 발목을 잡게 된다. 현실적으로, 우선순위를 설정하여 도메인 코드를 최우선으로 테스트하고 누가 봐도 자명하거나 주요 도메인이 아닌 계층은 테스트 작성을 건너뛰거나 우선순위를 낮추는 것이 좋다.
  • 커버리지 중복(overlapping overage)
    테스트의 단계별 종류가 일종의 집합과 같은 계층적 구조를 형성하기 때문에 기인하는 문제로, 하위 계층에서 충분히 테스트 된 내용을 상위 계층에서 또다시 테스트하기 때문에 발생하는 중복을 말한다. 예를 들어 상위 계층에서의 입력이 부분적으로 하위 계층으로 전달 또는 주입되는 경우, 단위 테스트 등으로 충분히 여러 케이스들을 검증한 상태라면 상위 계층 테스트에서도 이를 구태여 번복할 필요는 없다.

    예를 들어 비밀번호 입력 컴포넌트에서 '영문 대소문자, 특수문자가 없다면 입력 불가' 등의 요구사항과 각종 테스트 케이스들을 검증했다면, 이를 회원가입 페이지, 로그인 페이지, 비밀번호 초기화 페이지 등 모든 상위 계층에서 또다시 검증할 필요는 없다는 것. 물론 한다고 나쁜 건 아니지만, 이게 반복된다면 이미 검증된 커버리지 영역을 계속 덮어씌우는 것과 같기에 불필요하고, 추후 요구사항 변경시 함께 수정해야 하는 테스트가 늘어난다. 당장의 개발이 바쁘다면, 대부분의 검증은 유닛 단계에서 마무리하고 통합 테스트 이후부터는 몇몇개의 happy path만 테스트하는 것이 시간 비용상 현실적이다.
  • 버려지는 테스트
    테스트는 기능이 이나리 기능을 묘사(describe)하고 검증하기 위한 부수적 존재이므로, 비즈니스 로직이나 요구사항에 변동이 발생하면 항상 이를 추적하며 따라가야 한다. 기존 기능의 목적과 형식에 맞게 테스트 케이스들과 데이터 등을 작성하였다면 기능이 변경되었을 때 실패하는 기존 테스트 케이스들을 지우고 새 요구사항에 맞는 테스트 케이스들을 다시 새로 만들기 위해 많은 시간을 소모해야 한다.

    기능에서 테스트를 도출하고, 테스트에서 코드를 구현하는 TDD의 이론적 위계를 따르면, 코드의 변화로부터 기능을 보호할 수는 있지만 기능의 변화로부터 테스트를 보호하는 것은 근본적으로 불가능하다. 요구사항이 수시로 바뀌는 환경이라면 이상적인 TDD 이론은 테스트 코드의 유지보수 비용을 증가시켜 역효과를 낼 가능성도 있으므로, 도입에 신중해야 한다.
  • 종속성 분리에 소모되는 시간
    테스트하고자 하는 코드가 순수 도메인 로직이 아니라 네트워크, 데이터베이스, 시간 등 불순수한 종속성과 강하게 엮여 있다면, 단위 테스트 시 이를 일일히 분리하고 mocking하는 과정에서 의외로 많은 시간이 소모된다. 물론 이론적으로는 그러한 기술적 부채를 줄이는 것이 옳으나, 현실적으로 대부분의 코드는 일정 부분 이상 떼어내기 힘든 종속 관계를 이용하고 있고, 이를 일일히 injection 패턴으로 분리해내는 것은 정말로 많은 시간을 소모한다. 코드가 naive하면 테스트가 어려워지고, 테스트하기 좋게 만들면 코드가 복잡해지는 모순이 발생할 수 있다.
  • 추가적인 기록물의 증가
    주석의 치명적인 단점은 주석이 항상 코드와 동일한 내용을 보장하지 못한다는 것이다. 쉽게 말하면 소스코드에 쓰여진 주석의 내용과 TDD용으로 개발된 테스트와 실제 라이브 서비스에서 돌아가는 코드와 개발문서가 전부 다를 수도 있다. 추가적인 기록물이 늘어나는 만큼 추후에 읽고 검증해야할 기록물 또한 늘어난다.
  • 시간에 쫓기기 쉬움
    모든 일이 그렇듯 테스트를 하면서 개발할 여유조차 주어지지 않는 경우가 생긴다. 시스템 통합의 예를 보면 알겠지만, 대한민국의 소프트웨어 개발 환경은 대부분 TDD는 엄두도 못 낼 정도로 매우 열악한 편이다.

6. 테스팅 프레임워크

  • xUnit
    뒤에 -Unit이란 접두사가 붙는 여러 단위(unit) 테스트 프레임워크의 통칭이다. Java의 사실상 표준 테스트 프레임워크인 JUnit이 가장 유명하다. 최초의 xUnit 프레임워크는 SUnit인데, 켄트 벡이 Smalltalk용으로 개발했다. 이후 켄트 벡과 에릭 감마가 함께 이를 Java용인 JUnit으로 포팅했다. 비행기를 같이 타고 가다가 JUnit을 만들었다고 한다. 이 외에도 C++용의 CppUnit, .NET Framework용의 NUnit, XUnit 등이 있다.
  • Jest
    자바스크립트용 단위 테스트 프레임워크. Facebook에서 만들었다.

7. 코드 커버리지

테스트가 실제로 거쳐가는 코드가 몇 줄인지 확인하는 용도로 쓴다. 예외 처리 같은 것이 제대로 테스팅 되는지 확인하거나, 아니면 다른 개발자들이 제대로 테스팅 코드를 작성하는지 확인하려면 쓰는 것이 좋다. 프로파일러처럼 사실 유닛 테스트와는 독립적으로 동작한다. 그래서 본인이 어떤 코드를 써 놓고 코드가 어떻게 흘러 가는지 파악하는 데 사용할 수도 있다.

다만, 각종 프로파일러나 벤치마크 툴을 돌릴 때, 자체적으로 잡아먹는 성능 때문에 실제보다 성능이 낮게 측정되는 현상처럼, Coverage를 적용시키고 나면 테스트 속도가 느려질 수 있으므로 대형 프로젝트에서는 신중히 적용하자.

8. CI

위와 같은 프레임워크들을 활용해서 테스팅 케이스를 작성하고 나면 직접 돌려 보아야 하는데, 사람이 일일이 수동적으로 돌리려면 노동력이 들어가게 되고, 자동으로 돌린다 할지라도 서버를 구매할 여력이 되지 않는다면 클라우드 서버를 빌려야 한다.
보통 특별히 클라우드 서버를 빌려 쓰기 보다 CI를 사용하는 편이다.
개인 사용자나 오픈 소스 프로젝트의 경우 아예 무료로 쓸 수 있게 해주는 경우가 많으므로 잘 찾아보고 공부해 보도록 하자.
  • Travis CI
  • Azure Pipelines
  • CircleCI

[1] 그래도 영어 커뮤니티가 전부이다...