소프트웨어 테스트와 TDD
개발팀
2023. 2. 7.
테스팅은 필요한가?
“무죄가 입증될 때까지 모든 코드는 유죄다 (All code is guilty until proven innocent!)”
개발에 있어 소프트웨어 테스트는 개발만큼이나 중요합니다. 테스트는 소프트웨어가 실제 요구 사항을 충족하는지를 확인하고, 테스트를 거친 소프트웨어는 신뢰성, 보안 및 높은 성능을 보장하여 시간 절약, 고객 만족 및 비용의 효율화로 이어집니다.
오래전부터 테스트를 위한 다양한 방법론과 툴은 있었고 많이 권장되어 왔지만 여전히 대부분의 개발자는 번거롭고 성가신 일로 느낄 것입니다. 테스트 및 품질관리는 QA뿐만 아니라 개발자들도 같이 노력하고 진행해야 하는 프로세스로 개발자들은 유닛 테스트, 인테그레이션 테스트를 코드 작성과 함께 작성하도록 권장 또는 강조하고 있습니다.
테스트를 프로세스화하고 지키기 위한 여러 가지 방법론 중에 가장 유명하고 널리 사용되는 방법으로는 ‘Test Driven Development(TDD)’ 또는 ‘테스트 주도 개발’ 이 있습니다.
TDD란?
TDD는 ‘테스트 주도 개발’의 약자로 실제 코드 작성에 앞서 테스트를 먼저 작성하여 개발하는 소프트웨어 개발 방법론입니다. TDD는 extreme programming의 기법의 하나로 소개되었으며 최초의 extreme programmers 중 한 명인 Kent Beck에 기인하여 1990년도 후반에 제시된 프로세스로 알려져 있습니다.
TDD 이전에도 테스트 관행은 개발에 있어 이미 보편적이었습니다. 그럼에도 TDD가 새롭고 혁신적인 이유는 보통 테스트는 코드를 작성하고 그 후에 테스트를 작성하는 식으로 진행된 반면 TDD 방식은 코드 작성 이전에 테스트를 먼저 작성한다는 점 때문입니다.
TDD는 아래의 작은 질문에서 시작되었다고 합니다.
“If I test the code I write, I get better quality code. What would happen if I look the process to. he extreme: writing tests before the code itself?”
TDD는 디자인과 기술적으로 두 개의 다른 목표를 가지고 있습니다.
- 디자인 관점: 검증이 아닌 기능 사양의 명세를 목표로 합니다.
- 기술적 관점: 작동하는 깨끗한 코드(중복이 없고 명확한 코드)를 목표로 합니다.
TDD는 기존 프로세스의 문제를 보완하고 위의 두 가지 목표를 해결하기 위하여 나온 개발 방법론입니다.
TDD 개발 주기
초 단위 (나노 주기): TDD의 3가지 법칙 by Robert C. Martin
프로덕션 코드를 작성하기에 앞서 실패한 테스트 코드를 작성한다.
작성되어있는 테스트가 실패하거나 컴파일이 안 된다면 테스트를 추가 작성해서는 안 된다.
현재 실패한 테스트를 통과하기에 충분한 최소한의 코드만 작성한다.
위에서 제시된 방법을 따르게 된다면 한 번에 한 가지 테스트만을 작성하고 코드를 구현해가며 매우 짧은 반복 주기를 통하여 코드를 완성한다는 것을 알 수 있습니다.
이 세 가지 규칙은 나노 주기라 불리며 초 단위의 반복적인 테스트 주기를 가지게 됩니다. TDD의 창시자 Kent Beck과 클린 코드 책의 저자로 유명한 Robert C. Martin에 의하면 나노 주기의 3가지 규칙에 벗어난 개발 스타일을 금기시하고 있습니다.
분 단위 (마이크로 주기): Red-Green-Refactor
- 실패하는 테스트를 작성 [Red]
- 테스트를 통과시키기 위한 코드 작성 [Green]
- 통과된 코드를 리팩토링 [Refactor]
해당 방법의 철학은 개발자들이 개발에 있어 두 가지 목표를 동시에 추구할 수 없다는 생각에 기반합니다.
- 올바른 기능 작동
- 올바른 설계 구조
RGR 개발 사이클은 먼저 소프트웨어가 올바르게 작동하도록 개발하는 데 집중하도록 합니다. 그 후 작동하는 소프트웨어가 지속적으로 살아남을 수 있는 구조를 제공하는 데 집중합니다. 또한 실제 코드에 대해 기대되는 바를 더욱 명확하게 정의함으로써 불필요한 설계를 피하고, 최소한의 간결한 코드를 지향하게 합니다.
“Make it work. Make it right. Make it fast” - Kent Beck
Kent는 추가적으로 아래와 같이 말합니다.
“Write a test, make it run, make it right. To make it run, one is allowed to violate principles of good design. Making it right means to refactor it.” – KentBeck
Kent의 말을 종합적으로 살펴보면 Red 단계에서 Green 단계로 넘어가기 위하여 빠르게 선 개발을 진행하며 빠른 개발 과정에서 중복된 코드나 구조적 취약점 등의 문제점은 무시하고 진행해도 괜찮다고 말합니다. 또한 마지막 Refactor 단계에서 모든 디자인적 문제를 해결한다고 이야기하고 있습니다. 마이크로 주기의 반복적인 작업을 통해 코드 개발과 리팩토링을 동시에 진행하게 됩니다.
10분 단위 (밀리 주기): Specific/Generic
“As the tests get more specific, the code gets more generic.” - Robert C. Martin
테스트가 늘어남에 따라 테스트는 더욱 구체화 됩니다. 구체화 된다는 말은 행동에 관하여 보다 더 자세한 사양이 된다는 것입니다. 훌륭한 개발자는 코드의 범용성을 높여감으로써 기능 스펙의 정의를 내리게 됩니다. 이를 통하여 특정 목표를 이루기 위한 코드가 아니라 범용적으로 재사용할 수 있는 기능을 점점 늘려가게 됩니다.
시간 단위 (기본 주기): Boundaries
TDD의 마지막 기본 주기는 앞에서 실행되고 있는 모든 주기가 ‘Clean Architecture’로 가는 것을 보장하기 위한 주기입니다. 나노와 마이크로 주기를 반복하다 보면 시스템의 전체적인 그림을 보기 어려워집니다. 기본 주기를 매시간 가지면서 시스템의 전체적 구조가 올바른 방향으로 가는지 점검하는 시간을 가집니다.
TDD 개발 주기와 단위 테스트
TDD의 마이크로 주기(RGR 주기)에서는 단위 테스트로 이루어지는데 단위 테스트는 TDD가 지향하는 ‘Clean code that works’ 를 실현하기 위한 핵심입니다.
TDD를 테스트 기법으로 잘못 알고 있는 사람들이 있는데 TDD의 본질은 소프트웨어 개발론입니다. TDD의 본질은 유닛테스트를 작성하기 위한 프로세스가 아닌 RGR 주기를 통하여 ‘Clean code that works’를 이루기 위한 강력한 방법 중 하나이고, Refactoring과 작성된 테스트의 통과 과정을 거치면서 소프트웨어는 점점 작동하는 이상적인 설계를 이뤄갈 수 있습니다.
유닛 테스트의 규칙: F.I.R.S.T 규칙
Fast - 유닛 테스트는 빨라야 합니다.
각 유닛 테스트는 빨라야 합니다. 테스트의 실행속도가 느려지면 TDD 주기를 반복적으로 실행하는데 개발자는 부담을 느끼며 지키지 않게 됩니다. 그리고 프로젝트 전체 개발 속도에도 영향을 주게 됩니다.
Independent - 독립적이어야 합니다.
각 유닛 테스트 간 순서나 test data의 의존성이 있어서는 안 됩니다. 예를 들어 사용자의 로그인 기능을 위하여 유저를 생성하는 테스트가 선 진행되어야 한다는 식의 가정이 있어서는 안 됩니다. 만약 테스트 간에 의존성이 있다면 하나의 테스트 실패가 모든 테스트의 실패로 이어지며 실패의 원인을 규명하는데 어려움이 생깁니다.
Repeatable - 언제나 반복적으로 테스트 실행이 가능해야 합니다.
각 유닛 테스트는 언제나 몇 번이고 반복적으로 실행할 수 있어야 합니다. 예를 들어 첫 테스트에서 작성된 데이터에 의하여 두 번째 테스트가 실패하면 안 되며 또한 테스트는 어느 환경에서나 작동되도록 설계되어야 합니다. 인터넷이나 DB와 같은 외부 요인이 아닌 오직 작성된 코드의 로직만을 테스트하도록 설계해야 합니다.
Self-Validating - 자체 검증이 되어야 합니다.
테스트 자체가 최종적으로 pass/fail인지 판단이 되도록 자동화되어야 합니다. 테스트의 output이나 생성된 데이터를 보고 개발자가 자의적으로 pass/fail 여부를 수동으로 정의되지 않도록 작성되어야 합니다.
Timely - 실제 코드 작성 전에 작성이 되어야 합니다. TDD를 진행하기 위한 필수 규칙입니다.
일반 vs TDD 개발 방식의 비교
일반 개발 방식
보통 개발을 진행하는 방식은 요구사항 설계 -> 개발 -> 테스트 형태의 개발 사이클을 보입니다.
- 문제점
- 자체 버그 검출 능력 저하
- 소스 코드의 품질 저하
- 자체 테스트 비용의 증가
소프트웨어는 계속해서 진화해가며 새로운 기능 추가 및 리팩토링은 필수적으로 진행이 됩니다. 어느 프로젝트든 초기 설계에서 모든 것을 완벽하게 설계하기 힘듭니다. 요구사항은 지속적으로 변화하고 추가되며 소프트웨어는 설계를 변경해 가며 요구사항을 맞춰 지속적으로 변경되게 됩니다.
재설계 과정에서 사용하지 않는 코드나 중복된 기능들이 남게 되어 ‘code smell’이 점점 늘어가게 됩니다. 결과적으로 확장성은 점점 떨어지고, 코드는 복잡해져 관리 및 유지보수가 어려워집니다. 이런 상태의 코드는 작은 변화에도 ‘side effect’가 예측되지 않아 작은 수정에도 모든 부분을 테스트해야 하므로 테스트 비용이 점차 증가하며 기술적 부채가 쌓이게 됩니다. 결국 legacy 코드에 관하여 잘못된 코드도 고치지 않으려는 현상이 일어나고, 구조적인 결함도 수정하지 못하게 되는 경향이 있습니다.
TDD 개발 방식
- 테스트를 실제 구현되는 코드보다 먼저 작성
- 테스트를 실행
- 실제 코드 작성
- 테스트를 재실행, 테스트를 통과하는지 검증
- 리팩터 진행
- 반복
위의 프로세스를 지속적으로 진행하면 자연스럽게 코드의 버그가 줄어들고 개발 사이클 도중 추가되는 기능이나 버그 개선사항 반영 및 확인도 간결해집니다. 소프트웨어 요구사항들이 모두 테스트로 존재하고 작동 여부를 테스트할 수 있기에 기능 추가 및 리팩토링에 용이합니다.
“Test-driven development (TDD) is a way of managing fear during programming.” - Kent Beck
TDD는 개발자들이 코드를 변경하는 데 느끼는 두려움을 관리해줍니다. 변경하면서 발생하는 부작용들은 테스트를 통해 확인이 가능하여 좀 더 적극적으로 리팩토링에 임하기 쉽습니다. 또한 개발에 앞서 작성되고 관리되는 테스트로써 개발자들이 더 명확하게 스펙 정의 및 관리가 가능하며 간결하고 중복 없는 설계에 도달할 수 있도록 합니다.
결론
TDD는 강력한 소프트웨어 설계 툴로써 ‘Clean code that works’를 가능하게 해주며 빠르게 요구사항과 설계 변경이 필요한 최신 프로젝트에 ‘Clean architect’와 두려움 없는 리팩토링 과정을 제공합니다. TDD를 통하여 더 높은 품질과 빠른 기능 추가 및 코드의 유지력이 높아집니다.
“TDD를 도입한 소프트웨어는 약 15 ~ 35% 정도의 개발시간 증가, 결함률(버그)은 약 40 ~ 90% 정도 줄었습니다.” – Microsoft, IBM
*해당 콘텐츠는 저작권법에 의해 보호받는 저작물로 엘리스에 저작권이 있습니다.
*해당 콘텐츠는 사전 동의 없이 2차 가공 및 영리적인 이용을 금하고 있습니다.
- #Softwaretest
- #TDD