The Real TDD
작년에는 애자일 행사로 올해는 TDD를 주제로 돌아온 OKKYCON2018 에 참여하였다. 평소 테스트에 관심이 많았고 테스트를 잘하는 개발자가 되고 싶었다. 추가로 개발팀 모두가 테스트를 작성했으면 하는 바람에 플레이윙즈 개발팀 전원이 참석했다. 행사는 잠실 삼성 SDS에서 열렸고 장소, 식사, 간식 등 만족스러웠다.
모든 세션이 TDD를 대주제로 한 강연이었지만 발표자분들 각각 가지고 계신 노하우를 너무 잘 전수해주셔서 키워드로 정리해 보았다.
테스트가 어렵다 -> 테스트가 불가하다가 아니다.
테스트를 위한 준비과정이 많은 것이다.
테스트하기 쉬운 코드란
항상 같은 결과 반환.
외부 상태를 반영하지 않음.
테스트하기 어려운 코드란
항상 같을 결과를 반환하지 않는다.
외부 상태에 의존적인 코드.
Mock 사용하면
작성된 코드를 강제할 수 있다.
목 사용이 장점이 많지만 생각해볼 점이 있다.
행위 검증(mock) vs 상태 검증
행위가 호출되었는가 vs 결과값이 무엇인가
Mockist vs classicist
불필요한 추상화 유발 가능성 vs 불필요한 추상화 필요 없음
구현되지 않은 코드에 의존 가능 vs 구현된 코드에만 의존
mock객체 사용의 문제점(주관적 의견)
목을 남발할 가능성이 크다.
목 사용 예제는 간단하다. 그래서 장점이 크게 보인다.
적당 수의 목 사용에 대한 답을 찾기 어렵다.
상태 검증으로 돌아가 보자.
mock 객체 사용 문제점 극복 방안 (상태 검증 활용)
TDD를 통한 사전이 아니라 사후 테스트를 하자.
구현된 코드를 사용하지 않고 굳이 어려운 길을 택할 이유가 없다.
완벽을 추구하면서 목을 사용하는 비용을 들일 필요가 있는가.
정리
구현 코드는 테스트를 만족시킬 만큼만 작성하라.
구현 코드가 테스트 범위를 초과하는 만큼 작성되면 해당 코드는 코드 커버리지를 만족하지 않는다.
어려운 코드는 최대한 분리하여 최대한 가장자리에서 만나게 하자.
수동 테스트 + 자동 테스트를 적절히 활용하자.
Mock을 사용하여 테스트한다. Interface(이음새 - seam)를 사용하여 mocking 한다.
Interface를 DI(Dependency Injection) 한다.
테스트하기 쉬운 코드와 어려운 코드를 분리.
두 부류의 코드는 최대한 가장자리 위치.
가장자리를 테스트하는 방법을 익히자.
가장자리에서 맞물려 돌아가는 코드는 주로 수동 테스트를 한다.
두 부류의 코드 섞어놓고 테스트가 어렵다고 포기하지 말자.
TDD / 리팩토링 -> 연습(의식적인 연습 필요)
테스트하기 쉬운 코드와 테스트하기 어려운 코드를 보는 눈
테스트하기 어려운 코드를 테스트하기 쉬운 코드로 설계하는 센스가 필요
1단계 - 단위 테스트 연습
TDD를 하기 전에 단위 테스트를 먼저 연습하자.
내가 사용하는 api 사용법을 익히기 위한 학습 테스트에서 시작
예를 들어 string의 split 등 arraylist의 api 동작 방식 등.
Input과 output 이 명확한 util 성 클래스에서부터 시작하자.
알고리즘을 학습 시 구현에 대한 검증을 단위 테스트로 시작하자.
2단계 - TDD연습
원칙 1 - 회사 프로젝트에 연습하지 말고 토이 프로젝트를 활용해 연습하자.
원칙 2 - UI , DB 등 의존성을 가지지 않는 요구사항부터 연습.
계산기 요구사항 정의 및 연습 (예제)
TDD 사이클 (Test Fail - Test Passes - Refactor)
TDD 연습이 목적 난이도가 낮거나 자신에게 익숙한 문제로 시작하는 것을 추천
3단계 - 리팩토링 연습
리팩토링 - 메서드 분리
테스트 코드는 변경하지 말고 테스트 대상 코드를 개선하는 연습을 한다.
뎁스가 2단계 이상 들어가면 리팩토링 대상
Else를 안 쓰는 연습을 하자.
메서드가 한 가지 일만 하도록 구현하기
로컬 변수가 정말 필요한가?
Compose method 패턴 적용 등.
한 번에 한 가지 명확한 구체적인 목표를 가지고 연습하라.
연습은 극단적인 방벙으로 연습하는 것도 좋다. (메서드 라인 줄이기)
테스트가 뒷받침된 코드는 리팩토링이 안전하게 가능하다.
4단계 - 장난감 프로젝트 난이도 높이기
점진적으로 요구사항이 복잡한 프로그램을 구현한다.
앞에서 지켰던 원칙을 지키면서 구현한다.
게임과 같이 요구사항이 명확한 프로그램으로 연습한다.
의존관계(UI, DB 등)가 없는 프로그램으로 연습한다.
5단계 - 의존관계 추가를 통한 난이도 높이기
웹 , 모바일 , UI 등 의존관계를 추가한다.
테스트하기 쉬운 코드와 어려운 코드를 보는 눈
앞 단계 연습이 잘 되어있다면 분리하는 역량이 쌓였을 것이다.
한 단계 더 낳아간 연습을 하고 싶다면
컴파일 에러를 최소화하면서 리팩토링
ATDD 기반으로 응용 애플리케이션 개발
레거시 애플리케이션에 테스트 코드를 추가해 리팩토링하기
추천도서
1만 시간의 재발견
소트웍스 앤솔러지
클린코드
TDD / 리팩토링 연습을 위해 필요한 것은?
조급함 대신 마음의 여유
나만의 장난감 프로젝트
같은 과제를 반복적으로 구현할 수 있는 인내력
TDD Cycle (Red-Green-Refactor)
Red - Write test that fails
Green - write code to pass the test
Refactor - remove duplicate, etc.
TDD의 장점
동작하는 코드에 대한 자신감
회귀 테스트를 통한 자유로운 리팩터링
코드에 대한 지식이 증가
개발 생산성 향상
From Test First
과도한 설계를 피하고, 간결한 interface를 가짐
불필요한 기능을 줄임
실행 가능한 문서를 가짐
코드 품질을 높임
테스트 커버리지가 높을수록 퀄리티가 높다 (SonarQube를 이용하여)
소프트웨어 품질 = 외부 품질 + 내부 품질
품질 비교 사례 (코드 퀄리티)
1-1 테스트 추가하기
이미 테스트하기 쉽게 만들어져 있는 코드로 시작한다.
순수 함수 / 외부 의존성이 없는 코드를 먼저 시작한다.
유틸성 / helper 함수
프로덕션 코드는 수정하지 않고 함수 스펙에 맞는 테스트를 작성하고 검증한다.
테스트가 완료되면 테스트 코드는 절대 수정하지 않고 기존 코드를 리팩터링 한다.
테스트하기 어렵게 만들어져 있는 코드에서 테스트하기 쉬운 것만 분리한다.
선택 기준
중요도가 높은 비즈니스 로직이 포함된 부분
버그가 발견된 부분(과거 x)
결합이 낮고 논리는 복잡한 부분 -> 외부 의존도가 낮으면서 비즈니스 로직은 복잡한 함수
테스트하기 어려운 코드
API 요청 + 응답 결과에 따른 UI 차이 + 중요 비즈니스 로직
외부 의존성이 없는 중요 비즈니스 로직만 분리하여 독립적인 함수를 만들고 테스트를 작성한다.
1-2 새로운 코드는 TDD로 개발하기
비즈니스 요구사항 추가 ex) 약관에 모두 동의해야 가입 가능
비즈니스 요구사항에 맞는 테스트 코드 추가 -> 프로덕션 코드 작성
기존 코드에 테스트만 추가 + 리팩터링
새롭게 추가되는 요구사항은 TDD로
2-1 좋은 점
테스트를 작성함으로써 리팩터링 / 배포 / 새로운 요구사항에 대한 구현에 대한 불안감이 감소한다.
스펙 문서 기능
파라미터 / 함수의 시그니처 등을 확인할 때 테스트 코드를 통해서 확인 가능
디자인 개선 효과 - 디자인이 좋지 않은 문제의 함수를 발견할 수 있다.
학습 동기부여 - 디자인에 대한 피드백을 받을 수 있다.
안정성과 설계에 많은 관심을 가지게 됨 - 개발 생산성 향상
디버깅 시간이 현저하게 줄어든다 - 테스트 안 해서 아낀 시간 < 테스트 안 해서 나온 버그 고치는 시간
프로젝트 생산성 향상 - 비즈니스 로직의 허점을 사전에 발견하기도 한다.
집중력 향상 - 동시에 한 가지 이상의 일을 하지 않도록 통제해준다. 지금 당장 내가 해야 할 일에 더 집중하게 만들어준다.
2-2 실수
테스트 자체가 목적이 되어버림
무엇을 테스트해야 하는지 잘 모를 때 하는 실수
테스트 대상 오류
불필요한 테스트의 기준(주관적)
비즈니스와 관련된 버그를 낼 가능성이 낮거나 없고
테스트를 유지함으로써 얻는 이익 < 테스트 유지와 관리에 드는 비용 일 때
테스트가 단언하고 있는 내용이 사용자에게 중요한 가치를 주는 것이 아닐 때
필요하지만 검증 방식이 잘못된 테스트
Dom 구조 및 ui 구조는 변경 가능성이 높아서 깨지기 쉽다.
해결 : 특정 위치로 검증이 아니라 데이터를 검증한다.
검증력이 떨어지는 테스트
하나이상의 가능성을 열어 두고 있다.
여러 상황인데 같은 결과값을 반환할 경우
테스트 제목과 검증의 불일치
테스트 함수의 제목과 검증 항목을 일치시킨다.
테스트를 앞서가는 프로덕션 코드
테스트 함수는 1개만 테스트하는데 이미 프로덕션 코드는 10개에 대해 돌아가는 로직으로 구성된다.
TDD 원칙 : 테스트가 통과하는 최소한의 코드만 작성한다.
2-3 고민
테스트를 위해 만들어진 데이터 (fixture)
목 데이터를 어떻게 관리해야 하는지 고민 중.
함수를 많이 분리 -> 왠지 모르지만 불안함
계속 분리하는 게 맞는 건지?
더 가독성을 해치는 게 아닌가?
분리된 함수는 유기적으로 잘 동작하는가?
CTO 님 답변
추상화 수준이 낮다.
높은 응집과 낮은 결합 -> 낮은 결합은 달성했지만 높은 응집은 달성하지 못했다.
실무에서 TDD를 많이 하는가? >> 아니다
Unit Test를 작성을 하고 있는가? >> 할 때도 있고 안 할 때도 있다
Unit Test를 통해 얻고 있는 장점을 충분하게 느끼고 있느냐? >> 그렇다.
테스트 코드는 프로젝트 코드가 사용되는 최초의 장소이며 고객이다.
모든 역사는 테스트 코드부터 시작된다.
테스트 안 하는 이유 - 귀찮음 / 힘듦 / 재미없음
추상이란?
문맥 위에서 오직 관심 있는 것들에 대해서만 집중하 명확하게 하는 것
숨겨진 본질
낮은 추상화
들쭉날쭉 추상화
끊어진 논리
알 수 없는 의도
욕심쟁이
테스트가 실패하는 이유는 단 하나
하나의 테스트는 오직 한 가지만 검사해야 한다.
인지능력의 과부하
흩어진 코드와 데이터
매직넘버(테스트를 통과하기 위한 값들)
깨지기 쉬운 것들
높은 결합
낮은 응집
우리들의 테스트 코드의 가독성의 문제점
픽스처를 위한 테스트 코드가 너무 길다.
setUp Method 등을 활용하여 추상화한다.
Helper 메서드를 활용하여 픽스처를 생성하는 코드를 분리한다.
테스트는 한 가지 결과에 대해서만 테스트하도록 분리한다.
테스트를 통해 성장하고 배운다.
Science vs Engineering - 프로그래밍은 엔지니어링에 가깝다. 정해진 예산안에서 문제를 해결해야 한다.
모든 부분에 TDD를 적용하지 않는다. 서비스 안정성과 제품에 도움이 되지 않으면 TDD를 하지 않는다.
수동 + 자동 테스트를 적절히 활용하라.
우리가 보호해야 하는 것
도메인
우리가 제어할 수 없는 것
외부 세상 - 실세계 / 인프라 / 외부 서비스 / 레거시
설계
낮은 결합 / 높은 응집 / 도메인 모델 보호
설계를 테스트하라
너무 세세한 구현을 테스트할 때 테스트는 모두 다 깨진다.
구현 내부 사항을 모두 테스트하는 것이 아니라 인터페이스(설계)를 테스트해야 한다.
정보 숨김(Information Hiding)
어려운 설계 결정과 변경될 가능성이 높은 설계 결정들을 다른 모듈로부터 숨기는 것
레거시와 함께 살기
레거시로부터 분리된 레이어를 구성하고 내 코드만 테스트하는 방법을 선택
프로세스 - 점진적으로
반복 주기 - 계획
문화 - 공유 > 목표, 지식
아키텍처 - 낮은 결합, 높은 응집
도메인 모델과 플랫폼 -도메인 모델은 플랫폼에 독립적이어야 한다. 도메인 모델을 호스팅 하는 역할만 할 것
목적 - 소프트웨어 사용자에게 어떤 가치를 전달할 것인가?
분석 - 목적을 달성하기 위해 소프트웨어에 어떤 변경을 가할 것인가?
작업 설계
소프트웨어 변경은 어떤 세부 작업이 있는가?
각 작업들은 어떤 순서로 진행되어야 하는가?
각 작업은 누가 담당하는가?
기존에 테스트에 관한 지식을 블로그 + 도서 등을 통해 혼자 공부해서 테스트에 대한 방향성이라던지 고민거리를 공유할만한 자리가 없었다. 주변에 많이 작성하시는 개발자분들도 없을뿐더러 글쓴이 조차도 테스트가 몸에 숙련된 개발자가 아니기 때문에 어디 가서 토론을 하거나 그럴 기회가 없었는데 국내에서 TDD를 주제로 컨퍼런스가 열린 것은 이번이 처음이라서 매우 많은 기대를 하고 참가하였다. 세션별로 공감 가는 점들이 매우 많았고 내가 잘하고 있는 부분, 잘못 진행하고 있는 부분들을 많이 배울 수 있어서 매우 좋은 시간이었다.
한 가지 아쉬운 점은 이미 테스트 작성을 통해 삽질을 해봤거나, 관심이 많은 개발자에게는 굉장히 흥미 있는 시간이었겠지만 만약 내가 테스트에 대해 아무것도 모르는 개발자라고 가정했을 때는 약간 어려웠을 수도 있겠다는 생각이 들었다.
글쓴이도 아직 많은 경험을 해본 것은 아니지만 매우 짧은 기간이나마 개발 시 테스트를 작성했을 때 내가 실수를 일으킬 수 있는 수많은 부분에서 테스트가 도움을 주고, 코드 리팩터링에 자신감이 생기며, 디버깅 시간이 빨리지고, 그로 인해 개발 시간이 빨라지고 나의 칼퇴 시간을 보장하는 도구로서 좋은 장점들을 어떻게 하면 잘 전파하고 개발 문화로 자리 잡을 수 있을지 고민이 되는 시간이기도 하였다. TDD를 한다고, 테스트를 작성한다고 힙한 개발자가 되는 것이 아니라 제품을 개발함에 있어 엔지니어의 사명으로 테스트는 당연히 해야 하는 것으로 인식되는 개발 문화가 정착되었으면 좋겠다.