TDD를 배우는 과정에서 어려웠던 점과 해결책을 공유합니다.
🤷♂️ TDD가 뭔데?
테스트 주도 개발 (TEST DRIVEN DEVELOPMENT)
테스트 주도 개발이란 켄트 벡이 정의한 개발 프로세스로서
테스트 코드를 먼저 작성하고 해당 테스트를 통과하는 코드를 작성하는 과정을 짧고 빠르게 반복하는 SW 개발 프로세스
를 의미합니다.
TDD의 3단계
- RED -> GREEN -> REFACTORING 3개의 사이클을 반복하며 각각의 스텝이 의미하는 바는 아래와 같습니다.
1. RED : 실패하는 테스트 코드를 작성
2. GREEN : 테스트 코드를 어떻게든 성공하도록 수정
3 REFACTORING : 테스트 성공하는 채로 리팩토링으로 개선
👍 TDD의 장점
일반적으로 TDD를 통해 얻을 수 있는 것들은 아래와 같습니다.
- 😊 심리적 안정감과 자신감
- ⚖️ 테스트 코드로 코드의 안정성
- 🛠 리팩토링 하기 좋은 구조의 코드
- 🎯 테스트 코드 작성 과정에서 요구사항 도출
클린아키텍처 저자 엉클밥은 TDD로 짜는게 그냥 짜는 것 보다 빠르다
는
그야말로 🛢고인물스러운 주장을 합니다… 😇
😭 내가 TDD를 실패한 이유
TDD가 뭔지 학습한 뒤 본격적으로 연습해보고자 자신있게 프로젝트를 생성했지만..
정작 코드를 작성하려고 하자 손이 탁 멈춰버렸고 한줄도 제대로 짜지 못했습니다. 😢
분명 단위테스트를 작성하는 것에는 익숙했지만 이상하게 TDD를 의식하자 코드가 나오질 않았습니다.
🤔 왜 실패했을까?
이후에 다시 분석한 실패 요인은 3가지 였습니다.
- 너무 어려운 예제로 시작했다.
- 요구사항을 정리하지 않았다.
- 순환 논리의 오류에 빠졌다.
♻️ 순환 논리의 오류?
테스트 코드를 막상 짜려고하자 내 머릿속에서 이런 의문들이 떠오르고 혼란에 빠지고 말았습니다.😱
구현 코드가 있어야 그에 대한 테스트 코드를 작성할 수 있는거 아닌가?
구현코드 없이 테스트 코드를 어떻게 작성하지?
그렇다고 구현 코드를 작성하고 테스트코드를 작성하면 TDD가 아니잖아?
뭐 부터 시작하는게 맞는거야?
단순히 RED -> GREEN -> REFACTORING 3개의 스텝만으로는 설명이 충분치 않았습니다.
스텝별로 좀 더 상세히 알아보도록 합시다.
💁♂️ 실전 TDD
1. README에 요구사항을 한국어로 정리
문장으로 정리함으로써 요구사항을 구체화하고 도출해내는 과정입니다.
- 코드레벨에서 구현해야 할 요구사항을 한국어로 정리합니다.
- 최대한 많이, 구체적으로 적습니다.
- 클래스와 함수를 의식하지 말고 기능단위로 적습니다.
- 이후 여기서 적은 기능 요구사항들을 그대로 테스트 코드로 작성합니다.
2. 컴파일 에러가 나는 테스트코드를 작성
- 아직 작성하지 않은 클래스와 함수를 상상력으로 작성해나가며 테스트 코드를 작성합니다.
- 결과적으로 이 단계에서는 컴파일 에러가 나는 상태가 됩니다.
- 필자는 그냥 pseudo code라고 받아들이니까 편하게 짤 수 있었습니다.
3. 누락된 코드를 구현
- 미구현 된 코드를 채워나가며 컴파일 에러만 해결합니다.
- 즉 실행 가능한 (그러나 실패하는) 테스트 코드로 수정합니다.
- 내부 로직을 구현할 필요 없이 테스트 실행이 가능하게만 짜면 됩니다.
4. 테스트 실행해서 Fail을 확인
- 테스트를 실행하고 결과로 Fail 을 눈으로 확인합니다.
- 이 과정을 생략하고 싶은 유혹이 자주 오지만 반드시 확인해야 합니다.
✋ 잠깐! 왜 굳이 결과로 Fail을 봐야해?
Unit 테스트는 디폴트가 성공이므로 테스트 코드에 버그가 있어서 실수로 성공 해버린다면 이후에 다음 스텝에서 테스트가 성공해도 정말로 로직을 잘 짠건지 검증이 불가능해지기 때문!
5. 테스트 결과를 어떻게든 Success 로 만든다.
- 무슨 짓(죄악😈)을 해서라도 일단 테스트 결과가 Success가 나게끔 만듭니다.
- 결과적으로 테스트 코드가 잘 동작한다는 정도만 검증합니다.
😈 죄악이란?
- 코드 중복, 하드 코딩 등을 이용하여 그야말로 주어진 테스트만 간신히 성공 하게 만듭니다.
- 실제 코드 로직을 구현하는 대신 입력과 출력의 형식 정도만 맞춥니다.
예를 들면 아래와 같은 sum()
이란 함수를 검증한다고 가정합니다.
assertEquals(4, sum(2, 2))
이 경우에 죄악 예시는 sum()
이 4를 반환하도록 하드코딩 하는 것입니다.
fun sum(a: Int, b: Int): Int {
return 4
}
결과적으로 assertEquals(4, sum(2, 2))
테스트는 성공을 하게 됩니다.
6. 코드를 리팩토링
- 테스트 결과를 Success 유지하는 채로 코드를 리팩토링합니다.
- 전 단계에서 저지른 죄악(코드 중복, 하드 코딩 등)을 속죄하는 단계 👼
- 이 단계부터는 실제 상용 코드를 구현 합니다.
- 반복해서 테스트를 돌려가며 리팩토링 해나갑니다.
- 필요에 따라서는 테스트 케이스를 추가하고 프로세스를 반복합니다.
7. 반복
충분히 안심이 된다면 다음 로직을 2단계부터 반복합니다.
🚀 더 나은 TDD를 위한 Tip
- 연습이 많이 필요합니다.
- 테스트 짜기 쉬운 것 부터 시작해봅시다.
- 메소드추출, 클래스 생성 등 IDE 기능을 최대한 활용합니다. (젯브레인 충성충성 ^^7)
- ⚠️모든 케이스를 반드시 검증 해야하는 것은 아닙니다.
- 테스트 코드는 꼭 많은 것이 좋은 것은 아닙니다.
- 테스트 코드가 너무 많으면 오히려 리팩토링이 부담스러워 집니다.
- 본인 스스로 리팩토링 할 때 안심될 수준을 권장합니다.
- Private 함수는 테스트 할 필요가 없습니다.
📐 OOP vs TDD vs Refactoring 삼각관계
객체지향, TDD, 리팩토링은 모두 밀접한 연관관계를 갖게 됩니다.
프로젝트의 상황, 개발자의 역량과 숙련도에 따라 이 연관관계가 절망이 될 수도, 희망이 될 수도 있습니다.
절망편
희망편
🦄 결론
TDD는 많은 연습을 필요로 하지만 단계별로 차근차근 해본다면 어렵지 않을 거에요
여러분도 TDD를 통해서 안정적이고 리팩토링하기 좋은 코드를 해보시기 바랍니다. 😊