TDD 정의
- TDD(Test Driven Development)는 테스트가 개발을 이끌어가는 방법론이다.
- 즉, 테스트가 개발보다 선행하게 된다.
- 개발해야 하는 사항을 미리 정의하고, 각 기능의 입/출력을 미리 정한 뒤, 기능을 구현하는 프로세스는 아주 일반적인 개발 과정이다.
- 이때 TDD 방식으로 개발하게 된다면, 테스트 코드를 먼저 작성함으로써 개발사항과 각 기능의 입/출력 요구사항을 코드로 문서화한 후 기능을 개발하게 된다.
TDD 예제
- 간단한 쇼핑몰 웹 사이트의 백엔드 서버에서 로그인 기능을 개발해야 하는 상황을 가정할 것이다.
- 다음과 같은 요구사항이 있다.
POST /login
으로user_id
와user_password
를 json을 실어 요청하면,token
을 응답받아야 한다.token
은user_id
에"_verifed"
가 붙은 문자열이다.
- 이제 위 요구사항 구현을 TDD로 진행해본다.
1. 테스트 작성
- 위 요구사항을 테스트 코드로 작성하면 다음과 같다.
1 |
|
- 이 테스트 코드를 실행하면 당연히 실패할 것이다.
- 아직 테스트 코드만 작성하고, 테스트할 대상을 작성하지 않았기 때문이다.
2. 테스트 대상 구현
- 이제 테스트할 대상인 서버를 구현해본다.
1 |
|
- 테스트가 통과하도록 서버 로직을 구현했다.
- 이제 서버를 실행시킨 뒤, 테스트 코드를 실행하면 테스트는 성공할 것이다.
3. 테스트 대상 리팩토링
- 테스트는 성공해서 모든 일이 끝난 것처럼 보인다.
- 하지만 위 서버 코드는 현재 하나의 모듈에 너무 많은 내용을 담고있는 것 같아, SRP 원칙에 위배된다.
- 기존 코드를 좀 더 구조적으로 리팩토링하고 싶다.
- 그래서 다음처럼 코드를 분리해보려고 한다.
- http 요청과 응답을 주고 받는 책임을 담당하는 함수
- 로그인 로직을 실행을 담당하는 함수
- 토큰 생성 로직을 담당하는 함수
- 먼저 “로그인 로직을 실행을 담당하는 함수”를
login()
으로 정의한다. - 테스트 코드로
login()
함수의 입/출력을 정의하는 것으로 시작한다.
1 |
|
- 아직
login()
함수를 구현하지 않았기 때문에 위 두 테스트는 실패한다. - 이제
login
함수를 다음처럼 구현한다.
1 |
|
- 마지막으로 “토큰 생성 로직을 담당하는 함수”를
create_token()
으로 정의해본다. - 마찬가지로 테스트 코드로
create_token()
함수의 입/출력 정의하는 것으로 시작한다.
1 |
|
- 이제
create_token()
함수의 구현을 작성해본다.
1 |
|
- 마지막으로 http 요청과 응답을 주고 받는, 기존
login_endpoint
함수를 리팩토링한다.
1 |
|
- 이제 모두 구현되었기 때문에 작성한 모든 테스트는 성공한다.
- 예시에서 코드를 Top-Down 방식으로 작성하였다.
- Bottom-Up 방식으로 내부 로직부터 코드를 작성하는 방식으로도 진행할 수 있다.
TDD와 레드-그린-리팩토링
- 위 예제에서 진행한 방식은 TDD에서 말하는 “레드-그린-리팩토링”이다.
- 레드-그린 리팩토링의 과정은 다음과 같다.
- 테스트를 먼저 작성한다.
- 테스트할 대상은 아직 구현되지 않았으므로, 테스트는 실패한다.(실패는 보통 빨간색으로 표현된다.)
- 테스트가 통과되도록 코드를 작성한다.
- 구현이 완료되면 테스트는 성공한다.(성공은 보통 초록색으로 표현된다.)
- 기존 코드를 필요에 따라 리팩토링한다.
- 리팩토링은 기존 동작에 영향을 주면 안 된다. 다시 말하면, 입/출력은 변하지 않고, 내부적인 동작만 바꾸어야한다.
- 리팩토링은 종종 사이드이펙트를 불러오기도 한다.
- 하지만 테스트 코드로 이런 사이드 이펙트를 확인할 수 있다. 리팩토링하다가 동작에 문제가 생긴다면, 테스트는 실패할 것이다.
- 한편, 리팩토링을 성공적으로 했다면 기존 테스트 역시 성공한다.
- 이처럼 TDD를 하게 되면 실패 -> 성공 -> 리팩토링의 순환을 가지는 “레드-그린-리팩토링” 순서로 개발을 진행하게 된다.
로버트 마틴의 TDD 3가지 법칙
- 실패한 단위 테스트를 만들기 전에는 제품 코드를 만들지 않는다.
- 컴파일이 안 되거나 실패한 단위 테스트가 있으면 더 이상 단위 테스트를 만들지 않는다.
- 실패한 단위 테스트를 통과하는 이상의 제품 코드는 만들지 않는다.
TDD의 장단점
장점
- 개발하고자 하는 대상에서 기대하는 것을 테스트 코드로 미리 명확하게 정의할 수 있다.
- 소프트웨어를 개발할 때 중요한 것은 이 소프트웨어가 “어떻게”가 돌아가느냐가 아니라 “무엇을” 제공해줄 것이냐 이다.
- 테스트 코드로 먼저 작성하면 입/출력과 발생하는 예외를 무엇으로 정의해야 할지 먼저 명확하게 정의할 수 있다.
- 덕분에 좀 더 사용하는 쪽의 코드나 사람 입장에서 사용하기 좋은 코드를 작성할 수 있게 된다.
- 테스트를 훨씬 꼼꼼히 작성하게 된다.
- 테스트 코드는 사실 작성하기 번거로운 존재이다. 특히 구현을 먼저하고 테스트를 작성하면, 테스트 작성에 느슨해지고, 자칫 예외나 몇몇 시나리오에 대한 테스트를 놓칠 수 있다.
- TDD로 진행하게 되면, 테스트를 먼저 작성하기 때문에, 테스트 코드를 누락시킬 빈도가 낮아진다.
- 테스트 코드가 탄탄하게 있기 때문에, 리팩토링도 겁먹지 않고 진행할 수 있다.
- 테스트 코드가 깔끔한 코드 사용 문서가 된다.
- TDD에서 테스트 코드는 테스트할 대상의 구현을 모른 채 작성되기 때문에, 철저히 사용자 중심적으로 작성된다.
- 따라서 테스트 코드는 코드를 사용하기 위해 필요한 최소한의 내용만 담게된다.
단점
- 테스트가 가능하도록 코드를 설계하는 것은 어렵다.
- TDD를 진행하게 되면 모든 코드들을 테스트 가능하도록 설계해야한다.
- 테스트 가능하도록 코드를 설계하려면, 추상화, 의존성 주입 등을 잘 활용해야한다.
- 또한 테스트 환경을 제대로 구축하는 것(Docker compose, DB 데이터 초기화 등)의 작업은 꽤나 번거롭다.
- 익숙하지 않은 채 TDD를 진행하면, 개발 프로세스가 느려질 수 있다.
- 위에서 말했듯, 테스트 가능한 코드와 테스트 환경을 만드는 것 자체가 어려운 일이다.
- 구현 로직보다 테스트 코드를 작성하고 고민히는데 훨씬 시간이 많이 들 수 있다.
정리
- TDD는 테스트가 구현을 선행하는 개발 프로세스이다.
- TDD를 하게 되면 보통 레드-그린-리팩토링 순서로 개발을 진행하게된다.
- TDD를 하면 꼼꼼한 테스트를 통해 코드 품질과 테스트 코드의 문서화 품질이 올라간다.
- 하지만 그만큼 테스트 가능한 코드와 테스트 환경을 갖추는 일은 어렵다.