내가 TDD에 관심을 가지게 된 이유
내가 TDD에 관심을 가지게 된 이유
최초로 TDD에 대해 관심을 가지게 된 시기는 사내 클린 코드 교육 때였다. 신규 프로젝트에 투입되었고, 모든 개발자들은 사내 교육을 의무적으로 참가해야 했다. 교육은 단위 테스트 작성방법, 테스트 커버리지, 효율적인 리팩터링, 그리고 IDE의 유용한 리팩터링 기능 등으로 진행되었다. 별 기대 없이 시작했는데, 좋은 영향을 준 의미 있는 교육이었다. 교육의 모든 부분이 그랬는데, 단위 테스트 작성은 개발된 코드를 테스트할 때, 무식하게 디버깅 값을 콘솔에 찍거나 alert 메시지를 출력해서 보던 수동적인 테스트 방식을 자동화할 수 있다는 희망을 주었고, 테스트 코드 커버리지는 이런 자동화한 테스트 코드로 프로덕션 코드가 얼마나 커버 가능한지, 다시 말해 비즈니스 로직의 빈틈을 줄일 수 있다는 희망을 주었다. 리팩터링을 들은 후 단순히 코드를 작게 나누는 것이 아니라, 테스트 코드라는 든든한 방어막 안에서 효율적으로 코드를 재작성하고 추상화할 여지를 고민할 수 있었다.
TDD란 무엇인가
기존 소프트웨어 개발방식은
- 요구사항/문제 발생
- 기능 구현
- 테스트(콘솔 출력, 직접 실행, 데이터 확인, …)
- 에러 혹은 기능 결함이 발생하면 다시 2번으로
- 완료
문제점
- 개발기간이 길어질수록 목표의식이 희미해짐 => ‘내가 뭘 하고 있었지?’
- 작업 분량이 늘어날수록 확인이 어려워짐 => ‘이게 어딨더라?’
- 집중력이 필요해짐 => ‘앗! 로그가 지나갔다!’
- 논리적인 오류를 찾기 어렵다 => ‘이 값이 들어가면 나오는 게 맞나…?’
그래서 TDD가 뭐라고?
“Test teh program before you write it.”
프로그램을 만들기 전에 테스트를 먼저 해라.
- Kent Beck
- 프로그램을 작성하기 전에 테스트를 먼저 작성하는 것
- 문서를 만들어 머리로 생각하고 눈으로 확인할 것인가?
- 예상 결과를 코드로 표현하고, 자동으로 판단하게 할 것인가?
TDD가 필요한 상황
- 디버깅을 위해 콘솔 로그를 헤매고 있을 때
- 개발이 끝났지만 왠지 불안할 때
- 오랜만에 열어본 소스코드가 무슨 내용인지 기억 안 날 때
- 스펙 문서를 만들어야 할 때
TDD의 목표
Clean code that works.
잘 동작하는 깔끔한 코드
- Ron Jeffries
- 잘 동작하는 것은 물론이고 깔끔함도 개발 목표에 포함된다.
- 유지보수성
- 가독성
- 비용 및 안정
등을 한방에!
개발에 있어 테스트 주도 개발의 위치
- 개발자가 처음으로 수행하는 테스트
- 프로그래머 테스트? 단위(Unit) 테스트
테스트 주도 개발의 진행방식
- 질문(Ask) => 테스트를 통해 시스템에 질문한다. => 테스트 실패
- 응답(Respond) => 테스트를 통과하는 코드를 만들어 질문에 대답한다. => 테스트 성공
- 정제(Refine) => 아이디어를 통합하고, 불필요한 것은 제거하고 모호한 것은 명확히 해서 대답을 정제한다 => 리팩터링
- 반복(Repeat) => 다음 질문을 통해 대화를 계속한다.
TDD의 장점
- 개발의 방향을 잃지 않게 유지해준다.
- 품질 높은 소프트웨어 모듈 보유
- 자동화된 테스트 케이스 보유
- 사용 설명서 & 의사소통 수단
- 설계 개선 => 테스트 코드가 있기에 안심하고 리팩터링 가능
- 자주 성공한다 => 성취감
TDD의 단점
- 테스트 코드를 작성해야 함 => 개발도 바쁜데 테스트 코드를 작성하라고?
- TDD와 관련된 도구를 배워야 함(xUnit, …)
- 테스트 케이스를 잘 작성하기가 어렵다.
- 팀 내 모든 구성원에게 TDD를 전파하기가 어렵다
엉클 밥(Uncle Bob)의 TDD 원칙
- 실패하는 테스트를 작성하기 전까지 제품 코드를 작성하지 않는다.
- 실패하는 테스트를 한 번에 하나 이상 작성하지 않는다.
- 현재 실패하는 테스트를 통과하기에 충분한 정도를 넘어서는 코드를 작성하지 않는다.
하지만 나는 TDD가 어려웠다
이렇게 좋은 도구를 왜 여태 몰랐을까? 이클립스는 기본적으로 JUnit을 포함하고 있는데 왜 사용하지 않았을까? 실무 코드에서 테스트 코드를 작성하려는 순간 깨달았다.
1. 토이 프로젝트와 다르고 복잡한 실무
교육 예제는 교육의 효과를 극대화하기 위한 소스코드이다. 스프링 등의 온갖 사내 개발 플랫폼으로 버무려진 프로덕션 코드에 적용하려니 의존성을 해결하려다 지쳤다. 스프링 의존성에 대한 테스트 방법은 교육에서 가르쳐주지 않았다.
2. 모든 코드를 테스트해야 하는가
1번의 문제를 해결하기 위해, 프레임워크 의존성을 배제하고 순수한 비즈니스 코드를 추출했다. 그 후 테스트 코드를 직성 해야 하는데 int sum(int a, int b) { return a + b; }
같은 단순한 메서드만 있지 않았다. 어떤 메서드는 객체의 상태를 바꾸고 어떤 메서드는 파일을 읽어온다. 어떻게 해야 할지 난감헸다.
3. 일은 혼자 하는 게 아니다 보니
아무리 좋아도 혼자 하다 보면 지친다. 내가 힘들 때 주변의 도움을 받아야 하는데, 먼저 익혀서 전파해야지 라는 생각으로 혼자서 고민하다 지쳐버렸다.
4. 여유가 없는 개발환경
테스트 코드를 작성하는 것도 일이다. 익숙하지 않은 일을 해야 하는데 시간이 없다 보니 마음이 급해지고, 결국 테스트 코드를 건너뛰고 개발하게 되었다.
좋은 칼을 손에 쥐고 있는데 잘 다루지 못해 내가 다치는 것 같았다. 어떻게 잘 써볼까 고민하던 차에 TDD를 주제로 OKKYCON이 개최된다는 소식을 들었다. 운 좋게 참가할 수 있는 기회를 얻었고, 내가 가지고 있는 고민에 대해 어느 정도 답을 얻었다.
OKKYCON 2018 - Real TDD
- 점진적인 시작
- 알고리즘 문제 풀이
- 유틸리티 기능 테스트
- 테스트하기 쉬운 코드와 어려운 코드를 분리하자
- 쉬운 코드
- 외부 상태와 차단됨
- 입력과 출력이 명확한 코드
- 같은 입력에 대해 항상 같은 결과를 반환하는 코드
- 어려운 코드
- 외부 상태에 의존적인 코드 (Network I/O, Database, …)
- 항상 같은 결과를 반환하지 않는 코드 (객체 상태에 의존적인 코드)
- 쉬운 코드와 어려운 코드의 경계는 Mock을 적절히 이용하자
- 쉬운 코드
- 수동 테스트(기존 테스트 방식)를 적절히 이용하자
- 커버리지만을 위한 테스트 코드 작성은 금물
- 커버리지 100%를 KPI에 추가한다면…
발표 자료 및 후기
- 정진욱 - 테스트하기 쉬운 코드로 개발하기
- 박재성(자바지기) - 의식적인 연습으로 TDD, 리팩터링 연습하기
- 한성곤(삼성SDS) - 코드 품질을 위한 테스트 주도 개발
- 이혜승 - 테알못 신입은 어떻게 테스트를 시작했을까?
- 양완수 - 테스트를 돌보기 위한 매우 간단한 실천 방법들, 그리고 효과
- 이규원 - 당신들의 TDD가 실패하는 이유
- OKKYCON: 2018 요약 및 후기 by 플레이윙즈 이기영님
그래서 지금 내가 할 수 있는 건 뭘까? 테스트 코드 작성에 익숙해지자!
1. 테스트 코드와 함께하는 알고리즘 테스트
최근 IT업계의 유행인 코딩 테스트를 테스트 코드를 작성해서 풀어볼 수 있다. 코딩 테스트의 소스코드는 대부분 입력값과 반환 값이 명확하다. 아래 코드는 해커랭크의 Simple Array Sum 문제에 대한 소스코드이다. 예시를 위해 비교적 단순한 문제를 골랐지만, 알고리즘 테스트에서 테스트 코드를 작성하면 문제의 요구사항이나 제약조건 등을 코드로 나타낼 수 있다. 테스트 코드 작성을 연습하는데 좋은 예제이다.
public class Solution {
// 배열의 값을 모두 더한 결과를 반환
static int simpleArraySum(int[] ar) {
int total = 0;
for (int el : ar) {
total += el;
}
return total;
}
@Test
public void test_whenBasicTestCaseIsGiven_SolutionWillReturnRightAnswer() {
int[] arr = { 1, 2, 3, 4, 10, 11 };
assertEquals(31, simpleArraySum(arr)); // TRUE
}
@Test
public void test_whenAnotherTestCaseIsGiven_SolutionWillReturnRightAnswer() {
int[] arr = { 1, 1, 1, 1 };
assertEquals(4, simpleArraySum(arr)); // TRUE
}
}
2. Utility 성격의 라이브러리를 테스트해보자
실무에서 사용하거나 사용 예정인 유틸리티 혹은 라이브러리를 익히는데 테스트 코드를 사용해보자. 최근 Apache Commons Lang 라이브러리를 자주 사용하는데, StringUtils.isEmpty()
기능을 익히는데 테스트 코드를 작성했다. 레퍼런스의 내용을 실제 코드를 작성해보면서 테스트할 수 있다.
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class StringUtilsTests {
@Test
public void test_isEmpty() {
// GIVEN
final String NULL = null;
final String EMPTY_LITERAL = "";
final String EMPTY_INSTANCE = new String();
final String NOT_EMPTY_LITERAL = "not empty";
final String NOT_EMPTY_INSTANCE = new String("not empty");
assertTrue(StringUtils.isEmpty(NULL));
assertTrue(StringUtils.isEmpty(EMPTY_LITERAL));
assertTrue(StringUtils.isEmpty(EMPTY_INSTANCE));
assertFalse(StringUtils.isEmpty(NOT_EMPTY_INSTANCE));
assertFalse(StringUtils.isEmpty(NOT_EMPTY_LITERAL));
}
}
3. 실무에 JUnit을 점진적으로 적용해보자
이제 어느 정도 테스트 코드 작성에 익숙해졌다면 실무 소스코드에 테스트 코드를 작성해보자. 두 가지 경우가 존재하는데, 기존에 있는 코드를 커버하는 테스트 코드를 작성하는 것과 신규 개발에 대한 TDD 적용이 있을 것이다. 전자의 경우 기존 요구사항을 테스트 코드 작성으로 향후 리팩터링이나 요구사항 변경에 대한 기준이 될 수 있을 것이고 후자는 TDD 사이클을 적용할 기회가 될 것이다.
앞으로는
현재 알고리즘 테스트와 실무에서 사용 중인 유틸리티 라이브러리를 대상으로 단위 테스트 작성에 익숙해지는 중이다. 테스트 도구와 코드 작성이 더 익숙해지면, 실무 소스코드에서 테스트 가능한 코드를 분리하고 (리팩터링) 점진적으로 단위 테스트를 작성해볼 생각이다. 그리고 나뿐만이 아니고 함께 일하는 동료들에게도 TDD를 전파하고 싶다. 하지만 아래의 경우를 조심해야 할 것 같다.
애자일 코치로 활동하시는 김창준 님께선 OKKYCON 패널토론에서 (자세한 워딩이 기억나지 않지만) 이런 뉘앙스의 말씀을 하셨다.
한 사람이 TDD 전문가가 되어서 팀원들에게 TDD를 전파하면, 대게 그 팀의 구성원은 TDD에 대해 부정적이게 된다. 어떤 경우엔 그 전문가가 TDD 전담인력이 되기도 한다. 오히려 모두가 TDD를 모르는 상태에서 함께 시작하는 것이 더 좋은 효과를 본다. TDD가 실패하는 경우를 보면 애자일을 애자일스럽게 하지 않고, TDD를 TDD 스럽게 하지 않는다.
박재성 님도 억지로 주니어 혹은 후배들에게 TDD를 강요하지 않는다고 하셨다. 상사가 먼저 액션을 취하는 경우 주니어들의 거부감이 생겨 오히려 실패하는 경우를 많이 보셨다고 말씀하셨다.