TDD(Test-Driven Development) 방법론
TDD란?
- TDD(Test-Driven Development)는 소프트웨어 개발 방법론 중 하나로써 테스트 코드를 먼저 작성한 후, 구현 코드 작성 단계와 리팩토링 단계를 짧은 주기로 반복하여 개발하는 '테스트 주도 개발 방법론'
TDD를 통해 얻을 수 있는 것
목표
- 단기적인 목표와 장기적인 목표를 뚜렷하게 제시해 주고 올바르게 잡아 줌
리듬
- 반복되는 짧은 개발 패턴을 통해 개발 리듬을 만듦으로써 개발 집중력을 높여 줌
소통
- 테스트 코드는 사용설명서 혹은 API 문서으로서 의사소통의 도구로 활용 가능
개선
- TDD를 행함으로써 개발하고 있는 코드의 문제점을 빠르게 잡아낼 수 있음
성취감
- TDD의 짧은 사이클 내에서 테스트 코드를 통과하는 구현체 코드 작성을 통해, 무엇 인가를 완성했다는 성취감을 느낄 수 있음
TDD 실행 단계
RED
GREEN
REFACTOR
TDD에 관한 오해와 진실
오해
TDD는 비용이 더 들고, 결국 개발 속도를 저하시킨다
- 학습에 필요한 기간을 개발 기간으로 산정하기 때문에 발생하는 오해
- 개발 기간에 TDD 방법론을 학습하는 시간까지 포함 시키기 때문에 개발 기간이 늘어난다고 생각
- 예를 들어 Java Spring Boot를 이용해서 개발한다고 했을 때에는, 해당 프레임워크에 익숙하다는 가정 하에 개발 기간을 산정하고 만약 프레임워크 사용에 익숙하지 않다면 업무 이외의 시간에 학습을 진행하는 것이 일반적. TDD 방법론 역시 도구로 생각하여 익숙하다는 가정 하에 개발 기간을 산정해야 됨
- 실제 개발 기간이 길어질 순 있지만, 길어진 시간 만큼을 통합 과정에서 줄일 수 있음
코드 커버리지가 높으면 좋은 코드다
- 코드 커버리지가 높다고 해서 모든 논리적인 시나리오를 다 커버할 수 있는 것은 아님
- 오히려 코드 커버리지 기준을 맞추려 필요 없는 코드를 작성하는 경우가 발생할 수 있음
진압보다 예방에 소비되는 비용이 높다
- 문제 발생 전 예방하는 것이 더 좋음
- 진압 시 발생하는 재배포 과정의 비용이 예방을 위한 비용보다 더 높음
진실
TDD ≠ Unit Testing
- TDD
- XP(eXtreme Programming)의 대표적인 개발 방법론으로, 테스트 코드를 먼저 작성하고 이를 통과하도록 실제 코드를 작성
- TDD는 목적 코드의 원활한 도출을 위해 부분적으로 Unit Testing 이라는 독립적 프로세스 요소를 사용하여 테스트 코드를 작성하며 테스트 코드에 주요한 가치를 부여 하진 않음
- Unit Testing
- 검증이 필요한 코드에 대해 테스트 케이스를 작성하는 절차 또는 프로세스
- Unit Testing은 테스트 코드가 목적 코드의 완전성을 입증 해주기 때문에, 테스트 코드 그 자체만으로 주요한 가치가 있음
TDD는 설계 개선에 도움을 준다
Mock 객체, 언제 그리고 어떻게 사용할까?
문제에 봉착 했을 때 Mock으로 해결해보자
테스트에 필요한 인스턴스를 아직 개발하지 않았을 때
- 스펙에 담겨진 인터페이스를 정의
- 인터페이스를 기반으로 가상의 Mock 객체를 생성하여 테스트 코드를 작성
테스트에 필요한 인스턴스 상태를 만들기 어려울 때
- 테스트 코드를 작성하다 보면 수 많은 절차를 거쳐 테스트하기 위한 상태로 만들어야 할 때도 있음
- 수 많은 절차 끝에 최종적으로 필요한 값을 주는 객체를 Mock 객체로 작성하면, 꼭 복잡한 과정을 거치지 않더라도 기능의 원래 목적에 맞는지에 테스트할 수 있음
행위에 대한 검증이 필요할 때
- 테스트 코드를 작성하다 보면 때때로 결과 값 이외에도 본인이 의도한 프로세스를 제대로 따라 갔는지 검증하고 싶을 때가 있음
- A라는 인스턴스의 a메서드를 실행할 때 B라는 인스턴스를 넘긴다면, B의 b메서드를 정상적으로 실행하는지 검증할 수 있음
어라? 테스트 코드가 거짓말을 하네?
- Mock 객체의 남용의 위험성은 바로 테스트 코드의 가치를 깨뜨린다는 것
- Mock 객체가 거짓말을 하기 때문
- 예 : Mock 객체에선 50으로 테스트하고 있는데, 조건이 10보다 큰 경우에만 실행되는 코드의 경우인 경우. Mock 객체가 아닌 실제 객체(10 보다 작은 값이 들어 왔을 때)를 받았을 때 기대한 결과 값을 얻지 못할 수 있음
- 코드의 변경이 발생했을 때 Mock 객체로 검증되지 않는 경우에는 통합 테스트를 통해서 검증할 수 있다고 하지만 무리가 있음
Mock을 잘만 쓴다면
- Mock을 사용하면 아직 구현하지 않은 모듈이나 테스트에 필요한 객체를 생성해 테스트 환경을 손쉽게 구출할 수 있어 테스트할 기능에 좀 더 집중할 수 있음
- 지나치게 많은 Mock 객체가 필요하다면 코드가 리팩토링이 필요하진 않은지 확인할 필요가 있음
TDD, 올바른 사용과 사용 습관
Top-Down으로 방향을 잡고, Bottom-Up으로 구현에 집중하자
- TDD는 작은 단위로 시작해서 요구 사항을 충족시키는 방법론이라는 인식이 많음
- 작은 요구 사항을 찾고 해결해 나가는 것이 나쁜 것은 아니나, 구현체의 기능에 심취한 나머지 구현체의 목적에 집중하지 못하는 경향을 보이기도 함
- 이런 단점을 보완하기 위해 Top-Down Approach TDD의 관점과 행위가 필요
Top-Down Approach와 Bottom-Up Approach를 어떻게 구분하나?
- Top-Down Approach와 Bottom-Up Approach 각각은 장단점이 있으며 상호 보완적인 성향이 있음
- "디자인은 Top-Down으로, 기능은 Bottom-Up으로"
- Top-Down Approach
- 요구사항을 정리
- 해당 요구사항을 빈 Test Case로 만듦
- 요구 사항에 대한 분기가 생기는 부분과 맞닥뜨렸을 때에는 Negative한 조건에 대한 케이스로 테스트를 구현
- Bottom-Up Approach
- 절제의 미덕을 갖춘 Bottom-Up Approach를 작성하면서 모듈의 집중도와 안정성은 더욱 정교해지고 전반적인 흐름의 안정성까지도 보장
여러 시선을 이용해서 튼튼한 제품을 만들자
- Top-Down Approach TDD로 요구 사항을 확인한 다음, 자신이 고객의 요구 사항에서 어느 부분을 프로그래밍하고 있고 진행 상황이 어느 되었는지를 가늠
- 기능의 세부적인 구현이나 리스크가 될 만한 기능의 '프로토타이핑'을 Bottom-Up Approach TDD로 안정적인 구현을 도모
바보 단계 거치기
- 최적으로 바보 같은 코드가 된다면 부끄럽겠지만, 중간 과정에서 바보 같은 모습의 코드를 포함하는 것을 오히려 좋음
- 개발 속도를 높임
- 개발자의 정신 건강에 이로움
바보 단계가 뭐지?
- 바보 단계 : 매직 넘버, 중복 코드, 들쑥날쑥한 추상화 수준, 의도를 나타내지 못하는 이름, 많은 라인으로 이루어진 메서드 등으로 이루어진 코드
- 바로 생각나는 코드가 있지만, 아름답지 않다고 생각되는 경우.
- 우선은 동작하는 코드를 작성하고 리팩토링을 진행
한 번에 한 걸음씩 나아가기
- 실수를 전혀 하지 않을 뿐더러 작성하려는 도메인과 코드에 대한 지식이 완벽하다면 한꺼번에 완벽한 코드를 작성하는게 빠를 수 있음
- 하지만 누구나 실수를 하기 마련이며, 현업에서는 익숙한 도메인보다 그렇지 않은 도메인 코드를 작성하거나 익숙한 코드보다 새로운 코드를 만들어야 하는 경우도 빈번하기에 부족한 코드를 작성하고 개선해 나아가는 방식을 택하는 것을 추천
시나리오 구상하기
템플릿을 통한 자연스러움
- 구현하고자 하는 기능의 테스트 코드를 만듦
- Given : 특정 환경이나 값이 주어졌을 때
- When : 구현하고자 하는 기능을 실행시키면
- Then : 그에 따른 결과가 나와야 함
TDD의 단위 테스트를 문서화하자
시간이 지나면서 거짓말을 하는 문서
- 프로그램 구현체에 대한 문서화
- 시스템 소스를 직접 보지 않아도 문서만 보면 프로그램이 어떤 식으로 이루어져 있는지 한눈에 파악 가능
- 해당 언어를 몰라도 어느 정보 흐름을 파악할 수 있음
- 하지만 레거시 코드의 운영 업무를 진행하면서 코드 변경이 있을 경우, 보통 해당 코드는 수정하지만 이미 만들어진 문서를 수정하지는 않음
- 그렇게 힘들게 만들었던 문서들은 조금씩 거짓말을 하기 시작
- TDD를 통해 만들어진 테스트 코드
- 코드가 수정되면 테스트 에러가 발생하기 때문에 개발자는 자연스럽게 해당 코드를 수정
- 문서는 잘못 된 정보를 나타내더라도 오류를 알려주지 않지만, 테스트 코드는 오류를 프로그래머나 담당자에게 알려줌
살아 숨 쉬는 사용 설명서 - 테스트 코드
- 레거시 코드를 유지 보수 할 때 메소드를 직접 호출 했을 때 원하는 값이 나오지 않거나 사용 방법이 궁금할 경우, 프로젝트 전체에서 해당 메소드를 검색한 후 사용되는 부분을 확인하고 용도에 따라 리팩토링을 진행
- TDD를 통해 만들어진 테스트 코드가 해당 메소드가 사용되는 것을 더욱 효과적으로 보여줄 수 있는 샘플 코드
테스트 코드가 종이로 된 문서보다 좋은 두 가지 이유
해당 메소드의 예외 상황을 파악할 수 있다
- 메소드를 호출하기 위해서는 해당 메소드의 파라미터의 어떤 값이 필수 값인지, 데이터의 중복은 허용하는지, 기존 데이터의 값이 없을 경우에 처리는 어떻게 하는지 등의 파악이 필요
- 문서의 경우 유지 보수가 잘되기 어려움
- TDD를 통해서 만들어진 테스트 코드는 해당 메소드의 소스 코드가 변하거나 로직을 파악할 수 있는 기회를 제공하고, 구현체의 단위 테스트 이름으로 예외 사항이나 제반 사항을 파악할 수 있음
의존 관계를 파악할 수 있다
- 해당 메소드를 테스트하기 위해서 먼저 생성해야 하는 데이터가 있는 경우
- Given 구절에 해당하는 코드를 보고 메소드의 의존 관계를 파악할 수 있음
단위 테스트 격리에 대한 관점의 차이
모의 객체 스타일
- 협력 객체의 오류가 있을 경우에도 불구하고 단위 테스트 자체는 성공
- 오히려 오류가 난다면 수 많은 테스트들에 수정이 가해져야 한다는 단점을 지적
- 테스트의 목적이 협력 객체가 아닌 협력 객체를 이용하고 있는 객체에 있기 때문
- 격리성을 통한 명확한 모듈화 단위 테스트를 주장
고전 주의 스타일
- 협력 객체에 오류 로직이 포함되어 있다면 모든 단위 테스트들은 실패
- 실패 신호로써 객체들 간의 연관성을 다시 한 번 검토할 수 있음
- 실패한 테스트의 수정 역시 실제로 적용해보면 큰 시간 낭비를 일으키지 않음
- 하나의 테스트가 협력 객체들을 이용함으로써 협력 객체 역시 테스트하는 것처럼 될 수 있다는 것은 인정하지만 그 점이 오히려 장점이라고 주장
저자의 생각
- 테스트의 실패 신호를 십분 이용하는 편이, 비록 시간이 좀 더 소요 되더라도 불확실한 코드를 보완하는 효과를 보이는 점에서 더 낫다고 판단
- 테스트의 격리는 실력이 완벽 해지기 전까지는 불필요하다는 입장
다양한 시각, 점진적 사용, 그리고 성장
- 처음 만들었던 테스트들은 코드가 점점 구현 됨에 따라 다양한 상세 테스트들로 인해 역할의 중복이 일어나게 되고, 결국엔 삭제되는 경우도 있음
- 이와 같은 상황에서 단위 테스트는 장렬히 전사했지만, 설계나 구현체의 완성도는 향상 되곤 함
- 더불어 경험에 의한 지식의 수준도 향상 됨을 느낌
- 또한 Mock으로 구현되었던 부분이 실질적 구현체로 대체되는 경우도 다반사
- 테스트가 삭제 되는 것을 아까워 하거나 두려워 하지 마라!
참고
agile이랑 비슷하네요