개요
TDD에 대해 이해한다.
Checklist
TDD는 소프트웨어 개발 프로세스, 혹은 프로세스 개발 방법론을 일컫는다.
개발자가 설계하고 이를 구현하는 과정에서 요구사항을 검증하는 테스트(테스트 케이스)를 적용한 개발 과정을 TDD라 한다.
개발자들 사이에서 TDD는 보통 각 항목에 적용하는 test(unit test, functional test 등)를 지칭한다.
try-catch
를 생각하면 TDD를 이해하는데 도움이 된다.
javascript에서 많이 사용하는 오류검출구문은 try-catch, try logic이 제대로 작동하지 않을 경우 err인자를 넘겨받아 catch logic이 동작한다.
TDD도 구조적으로 거의 비슷한 원리로 작동한다.
즉 test 항목에서 logic을 실행한 후에 TDD(assertion()) logic이 동작한다.
다만 try-catch는 오류가 있을 경우에만 catch문을 실행하지만, TDD는 logic을 실행하고 난 이후에 오류가 있든 없든 무조건 test logic을 실행한다.
test logic을 수행하면서 오류가 없다면 OK(django에서 TDD는 OK를 출력), 오류가 있다면 해당 오류에 대한 내용을 출력한다.
이때 test logic은, 우리가 작성해주어야했던 catch와 달리 이미 정해져 있는 logic을 수행할 수도 있다(마치 API처럼).
def test_two_past_questions(self):
"""
Questions would be displayed with sorted by pub_date
"""
question1 = create_question("test", datetime.timedelta(days=-30))
question2 = create_question("test", datetime.timedelta(days=-5))
response = self.client.get("/polls/")
self.assertEqual(response.status_code, 200)
self.assertQuerysetEqual(response.context['question_list'], [question2, question1])
위 코드는 python
에 TDD를 적용한 경우이다.
아래 self.assertEqual / self.assertQuerysetEqual 구문이 test logic에 해당한다.
그 이전의 모든 logic을 수행한 이후에 test를 진행하는데, 각각의 test logic(status_code가 일치하는지, 해당 문자열(text)이 response에 포함되어있는지)은 이미 정해져 있다.
장점
→ 철저한 객체 지향적인 코딩과 유지보수의 간편함
TDD가 적용되는 환경은 철저히 기능이 분리되고, 독립적으로 해당 기능만을 수행할 수 있도록 module화된 곳이다.
즉 TDD를 도입하게되면 unit test를 진행하기 위해 코드가 철저히 module화, 기능분리된다.
따라서 각 코드의 종속성이 낮아지게 되어 재사용 및 수정이 간편해지고, 그만큼 유지보수가 편리해진다.
→ 재설계 시간의 단축
객체지향적인 코딩은 결국 어떠한 기능을 구현하는가, 어떠한 객체를 구현하는가에 대한 명확한 정의가 이루어진다.
이를 통해 전반적으로 설계가 다시 이루어지는 번거로움을 방지할 수 있고, 다시 이루어진다 하더라도 TDD를 적용하기 전보다 훨씬 유연한 대응이 가능하다.
또한 각 기능 및 module이 분리되어 logic을 추가 구현하는 것이 간편하다.
→ 디버깅 시간의 단축
기본적으로 TDD는 동적 분석이 아닌, 정적 분석이다.
동적 분석의 경우 러닝(실행 이후)타임에 오류를 탐색하게되는데, 이 경우 어떤 시점에서 오류가 발생하였는지 파악하기위해 모든 코드를 분석해야 한다
반면 정적 분석은 말 그대로 test 대상을 실행하기 전에 분석하며, unit test를 통해 특정 오류나 문제점을 찾는게 용이해진다.
단점
→ 개발 소요시간 증가
TDD를 적용하기 위해선 기본적으로 코드가 길어지고, 전체적으로 리소스(인력, 노력)가 많이 투입된다(TDD자체가 단순한 작업이 아닌, 모든 코드를 손봐야 하는 번거로운 작업).
TDD자체는 이론·원칙적으로 중요한 부분이긴 하지만, 초기 개발비용이나 교육(실무에서 다룰 수 있을 정도까지)을 감당할 수 있어야 진행이 가능하다.
웹개발 프로젝트(SI)나 개발속도를 중요시하는 회사라면 TDD를 도입하는게 어려울 수 있다.
→ 러닝커브
일단 TDD는 이에 능숙한 시니어개발자가 주도하면서 진행이 되어야 하고, 미숙한 인원들을 교육해야 한다.
여기서 TDD 러닝커브로 인한 시간 및 자원소모가 발생한다.
또한 위 그래프처럼 프로젝트 초기 코드가 많이 재활용되고 버려지는 시점에서 TDD를 적용하면 시간소모가 늘어나고, 특히 오히려 TDD를 적용하지 않는 코드보다 더 크다.
따라서 프로젝트, 사업의 규모, 러닝커브를 적절하게 고려해서 TDD를 적절한 시기에 적용하는 것이 중요하다
TDD가 자동화 테스트 개념에 포함되기는 한다.
위에서 기술하였듯이 코드가 오래 유지되는 환경에서 유지보수가 매우 간편해지고, 코드 변경이 용이하다.
다만 코드작업초기, 안전함보다 속도가 중요시된다면 TDD를 적용하는 것이 어려울 수 있다.
하지만 TDD는 선택의 문제가 아닌, 도입 시기의 문제이다.
프로젝트를 장기화할 경우 TDD가 무조건 유리하고, non TDD에 비해 자원절약을 할 수 있기 때문에 언젠가는 반드시 도입해야하는 문제이다.
테스트의 난이도, 적용범위 및 역할 등에 따라 4가지 테스트로 나뉜다.
→ Static : 정적분석, 문법/변수관련 오류 탐색
→ Unit : 각각의 독립적인 동작(logic, class) 수행 관련 오류 탐색
→ Functional : 각 Unit들의 조화, 하나의 기능으로 잘 수행하는지 관련 오류 탐색
→ End to End (E2E) : 최종 결과물, 인터페이스 및 전체적인 내부 동작 관련 오류 탐색
TDD의 핵심은 Unit test라 해도 무방하다.
모든 Unit test가 잘 동작해야 Functional - E2E test가 문제없이 동작할 수 있다.
또한 개발 및 설계과정 자체가 Unit을 단위로 이루어지기 때문에, 실무에서 고민해야 하는 부분은 Unit test과 관련한 곳이다.
전통적인 test 기법은 void function과 같은 상태(state)가 없는 경우, 상태기반 테스트는 가능하지만 행위기반 테스트는 불가능한 단점이 있었다.
행위기반 테스트, 즉 상태가 없는 객체의 동작 수행여부를 검증하기 위해 대신 행위를 검증해주는 Mock 객체가 여기서 고안되었다.
좁은 의미의 Stub은 Mock 객체를 구현하기 위해 필요한 기능들 중 하나로, test에 필요한 Mock 객체의 동작을 지정해주는 과정을 말한다.
넓은 의미로 보면 객체에 필요 기능만을 구현하고 특정 상태를 반환하여, 상태기반 검증을 위해 사용하는 객체를 일컫는다.
dummy
가장 기본적인 test double이다.
단순히 객체(상태) 자체가 필요한 경우에 사용하며, 내부적인 정상동작은 보장하지 않는다.
stub
dummy 객체에서 특정 행동을 구현해놓은 객체이다.
기본적인 class나 인터페이스가 구성되어 동작까지 가능한 객체이며, DB나 외부입출력 장치와 연결되었으나 아직 구현이 미비할때 하드코딩한 요소라 할 수 있다.
미리 준비된 요소에 대해서만 검증이 가능하고, 해당 부분에 대해서만 import하여 test하기 위해 생성한다.
상태기반 검증을 이해하기 위한 기본 개념이다.
Mock
행위, 동작 등 특정 상황에 대한 테스트를 진행하기 위한 객체이다.
행위기반테스트의 상황이 만들어졌을때 Mock 객체를 활용하여 test를 진행한다.
※ 환경 구축을 위한 시간이 오래 걸릴 경우
※ 특정 모듈이 없어 테스트 검증 객체(DOC)이 없을 경우
※ 타 부서와의 긴급한 협업이 필요한 경우
※ 테스트 시간이 오래 걸릴 경우
행위가 정상적으로 작동하는지를 확인하기 때문에, 특정 상황에 대한 호출을 생각하면서 객체를 생성한다.
Jest는 페이스북에서 만든 javascript 전용 테스트 프레임워크이다.
문서화가 잘되어있고, 그만큼 API가 체계적으로 정리되어있어 많이 활용한다.
전용 패키지가 있어 npm을 통해 설치한 후, 특정 테스트를 위한 기능(메소드)을 활용하여 예상하는 결과(상태 혹은 행동의 결과)가 출력되는지 확인하는 방식으로 진행한다.
karma, mocha, enzyme, jasmine 등이 있다.
chrome.exe를 활용해서 브라우저 환경에서 test에서 기대하는 결과가 존재하는지, 출력되는지 확인하는 방식의 프레임워크이다.
Puppeteer를 활용하여 TDD를 진행할 경우, 내부적으로 브라우저 환경에서 실제 구현이 이루어졌는지 확인하기 위해 크롬 브라우저를 실행한다.
그 후에 빠른 시간동안 test를 수행하고, 브라우저를 닫는다.
다른 화면을 표시하는 TDD방식과는 달리, CLI를 통한 접근이 가능하며 구현된 화면에 별도로 나타내지는 않는다.
참고개념
테스트를 진행하는 의존 구성요소(DOC) 사용이 힘들때, 테스트 기능을 대신 수행해주는 객체를 일컫는다.
객체의 상태, 특징에 따라 Dummy, Stub, Fake, Spy, Mock 등이 있다.
일반적으로 test double은 상태기반 테스트로 작성, Mock object를 통한 테스트는 행위기반 테스트로 작성한다.
state base test, 테스트 대상 객체에 연관된 메소드들을 호출한 후 객체의 상태를 확인하는 테스트 기법이다.
예를 들어 setValue
로 value 변수의 상태를 변화(null or 0 → assigned)했다면, 이를 getValue
로 예상값(혹은 예상 상태)를 확인하는 형식이다.
가장 전형적이면서 전통적인 테스트 케이스 작성 방법이다.
behavior base test, 테스트 대상 객체의 logic이 올바르게 수행했는지(동작수행여부)를 확인하는 테스트 기법이다.
상태기반테스트와 달리 상태결과만을 확인하면 올바른 logic 수행 결과를 확인하지 못할 수 있다.
전통적인 test case 작성 시 void 함수와 같이 return 형태가 존재하지 않는 경우에, 상태기반 테스트를 통해 정상적인 상태나 logic 수행여부를 확인하기가 어려운 경우가 많았다.
즉 구체적인 반환값, 즉 상태를 가지지 않는 객체가 있다면 테스트 적용을 하기가 어려웠다.
이러한 상황에서 테스트를 진행하기 위해 상태가 없는 객체를 대신하여 test spy 객체를 만들거나, 자체적으로 검증을 해주는 Mock 객체를 만들어 test case를 작성하게 되었다.
정리
TDD 개념
https://ko.wikipedia.org/wiki/%ED%85%8C%EC%8A%A4%ED%8A%B8_%EC%A3%BC%EB%8F%84_%EA%B0%9C%EB%B0%9C
https://wooaoe.tistory.com/33
TDD level
https://kentcdodds.com/blog/static-vs-unit-vs-integration-vs-e2e-tests
TDD 러닝커브, 스타트업에 TDD를 반드시 도입해야 하는가
http://www.fransekman.com/should-startups-do-tdd/
TDD vs nonTDD cost
https://www.freecodecamp.org/news/isnt-tdd-test-driven-development-twice-the-work-why-should-you-care-4ddcabeb3df9/
test double
https://blog.pragmatists.com/test-doubles-fakes-mocks-and-stubs-1a7491dfa3da
test double / 관련 객체 개념(Mock/Stub)
https://tecoble.techcourse.co.kr/post/2020-09-19-what-is-test-double/
https://beomseok95.tistory.com/295
상태기반 테스트
https://www.whiteship.me/-ec-83-81-ed-83-9c--ea-b8-b0-eb-b0-98--ed-85-8c-ec-8a-a4-ed-8a-b8-eb-9e-80/