Search
Duplicate

TDD 화폐예제를 통한 테스트 실습

목차

개요

테스트에 대한 지식이나 방법을 텍스트로만 접해서는 한계가 있고, 아직 감이 오지 않을 수 있다.
해당 포스팅은 켄트벡 옹의 저서 TDDBE를 기반으로 화폐 예제를 직접 테스트에 의해 주도되는 코드를 작성하고 기능을 개발할 것이다.
이번 포스팅을 보는 사람들은 TDD의 리듬을 파악할 수 있기를 바란다.

간략하게 보는 TDD 사이클

1.
테스트를 작성한다.
2.
모든 테스트를 실행 후 새로 추가한 테스트가 실패하는지 확인한다.
3.
코드를 조금 바꾼다.
4.
모든 테스트를 실행하고 모두 성공하는지 확인한다.
5.
리팩토링을 통해 중복을 제거한다.

시나리오

미국 통화기준인 달러로 명명된 채권만 다루는 솔루션에서 다른 화폐로도 채권을 다뤄야하도록 프로젝트를 리팩토링을 해야 한다.
다중통화를 지원하는 보고서는 다음과 같다.
종목
가격
합계
IBM
1000
25
25000
GE
400
100
40000
합계
65000
다중 통화를 지원하는 보고서를 만들기 위한 통화 단위 추가
종목
가격
합계
IBM
1000
25USD
25000USD
Novartis
400
150CHF
60000CHF
합계
65000USD
환율 표
기준
변환
환율
CHF
USD
1.5
어떤 테스트를 작성하고, 이를 모두 통과하면 위에서 말한 시나리오를 모두 완성하고 코드가 완성되었음을 확신할 수 있을까? 우선 다음 두 가지 기능은 제공을 해야 할 것이다.
통화가 다른 두 금액을 더해 주어진 환율에 맞기 변한 금액을 얻을 수 있어야 한다.
어떤 금액(주가)을 어떤 수(주식의 수)에 곱한 금액을 결과로 얻을 수 있어야 한다.
TDD Pattern 에서 다뤘지만, 하나의 기능에 집중할 수 있고, 언제 모든 작업들이 끝났는지와, 번뜩이는 아이디어를 잊지않을 수 있도록 할 일 목록을 작성할 것이고, 현재 진행중인 할 일에 대해서는 굵은 글씨체로 끝난 할 일에 대해서는 줄을 긋도록 하자.
1. $5 + 10CHF = $10 (환율이 2:1인 경우) 2. $5 x 2 = $10
Plain Text
복사
할 일 목록

1. $5 x 2 = $10

위에서 말했듯이 할 일 목록에서 굵은 글씨체로 표현된 곱하기를 먼저 해결해보자.
여기서 자연스럽게 어떤 객체를 만들어야 할까 고민하겠지만, 그 전에 테스트를 먼저 만들어야 한다는 사실을 잊어서는 안된다. 다시 말하지만, 객체보단 테스트 작성이 먼저다.

테스트 작성

우선 오퍼레이션의 완벽한 인터페이스에 대해 상상을 해보자.
즉, 외부에 노출되는 최선의 API에서 시작해서 작업을 시작하는게 낫다.
@Test void multiplyTest() { //given Dollar five = new Dollar(5); //when five.times(2); //then assertThat(five.amount).isEqualTo(10); }
Java
복사
이 테스트 코드를 보면 의아한 부분과 태클을 걸고싶은 부분들이 막 생겨날 것이다.
필드에 직접 접근을 하고, 금액을 정수형으로 작성하고, Dollor 객체에 어떤 부작용이 있을지에 대해서 말이다. 하지만, 이런 부분들은 당장 신경쓰지 말자. 아니 할 일 목록에 추가한 뒤 당장의 문제를 해결한 다음 생각하자. 우선 할 일 목록을 업데이트 하자.
1. $5 + 10CHF = $10 (환율이 2:1인 경우) 2. $5 x 2 = $10 3. amount의 접근제어자를 private로 만들기 4. Dollar의 부작용 확인하기(side effect) 5. Money 반올림 확인하기
Plain Text
복사
할 일 목록 - 두 번째 업데이트
할 일 목록을 업데이트 했다면, 이제 다시 곱하기를 만들어보자. 일단, 내가 기존에 작성한 테스트코드는 컴파일도 되지 않을 것이다. 현재 발생하는 컴파일 에러는 다음과 같다.
Dollar 클래스가 없다.
생성자가 없다.
times(int) 메서드가 없다.
amount 필드가 없다.

컴파일 에러 해결하기

그럼 이제 이 에러들을 하나씩 해결해보자.
1.
Dollar 클래스가 없다.
: 그럼 Dollar 클래스를 만들자.
class Dollar {}
Java
복사
2.
생성자가 없다.
: 생성자를 만들어주자. 우선은 컴파일 에러를 해결하는게 먼저기에 아무 로직도 없다.
class Dollar { Dollar(int amount){} }
Java
복사
3.
times(int) 메서드가 없다.
: 메서드의 스텁(stub)을 작성해 줍시다. 컴파일만 되는것을 목표로 합니다.
class Dollar { Dollar(int amount) {} void times(int multiplier) {} }
Java
복사
4.
amount 필드가 없다.
: Dollar 객체에 amount 인스턴스 필드를 만들어 줍시다.
class Dollar { int amount; Dollar(int amount) {} void times(int multiplier) {} }
Java
복사
이제 컴파일 에러는 발생하지 않고 정상적으로 테스트가 실행 될 것이고, 콘솔창에 빨간불이 들어오는 것을 확인할 수 있다.
IntelliJ 기준 테스트 실패 콘솔 화면 10을 기대했는데 0이 나왔다는 내용이다.
빨간불이 나왔다고 슬퍼할 필요는 없다. 긍정적으로 생각해보자. 컴파일은 된게 어디야!

테스트 통과하기

이제 테스트를 어떻게 통과할지 고민해볼 수 있다.
가장 최소한의 작업으로 현재 테스트를 통과하기 위해서는 다음과 같은 방식을 생각해 볼 수 있을 것 같다.
class Dollar { int amount = 10; Dollar(int amount) {} void times(int multiplier) {} }
Java
복사
int amount = 10; 으로 초기화해줬다.
이제 통과하는 테스트!
이제 끝일까? 기뻐하기는 아직 이르다. 당장 amount만 해도 10만을 반환하기에 문제가 있을 것이라는 것을 추측하기 쉽다. TDD의 주기는 다음과 같으니 잊지 말자.
1.
작은 테스트를 하나 추가한다.
2.
모든 테스트를 실행해서 테스트가 실패하는 것을 확인
3.
조금 수정한다.
4.
모든 테스트를 실행해 테스트가 성공하는 것을 확인한다.
5.
중복을 제거하기 위해 리팩토링한다.

리팩토링

그럼 우리가 지금까지 한 것은 몇 번까지일까? 조금 수정해서 컴파일 에러를 수정하고 테스트 케이스를 통과하도록 했으니 1~4번까지 수행했다고 볼 수 있다. 그럼 이제 리팩토링을 수행해야하는데,
리팩토링이란 코드의 입력과 출력은 동일하게 유지하되 내부 구조를 변경하는 작업을 의미한다는 것을 잊지말자.

1. 중복 제거하기

너무나도 간단한 테스트와 Dollar 객체 사이에 어떤 중복이 있는지 찾아보자.
class Dollar { int amount = 10; Dollar(int amount) {} void times(int multiplier) {} }
Java
복사
@Test void multiplyTest() { Dollar five = new Dollar(5); five.times(2); assertThat(five.amount).isEqualTo(10); }
Java
복사
중복을 아직 못찾겠다면, int amount = 10;int amount = 2 * 5; 라고 생각해보자. 이제 중복을 찾을 수 있겠는가? five.times(2) 의 2라는 인수에서 중복을 찾을 수 있다.
안타깝지만 2와 5를 한 번에 제거할 수 있는 방법은 없다. 대신 객체의 초기화 단계에서 수행되는 amount값의 초기화를 times 메서드에서 수행하도록 해보자.
class Dollar { int amount; Dollar(int amount) {} void times(int multiplier) { amount = 5 * 2; } }
Java
복사
이제 인자 multiplier의 값이 2이므로 상수 2를 대체할 수 있을 것 같다.
class Dollar { int amount; Dollar(int amount) {} void times(int multiplier) { amount = 5 * multiplier; } }
Java
복사
아직 리팩토링할 여지가 있는지 살펴보자. 상수 5는 무엇을 의미할까? 테스트를 보면 생성자에서 넘겨주는 것을 볼 수 있는데, 이 값도 중복으로 제거할 수 있다.
class Dollar { int amount; Dollar(int amount) { this.amount = amount; } void times(int multiplier) { amount *= multiplier; } }
Java
복사
이제 곱하기에 대한 테스트 작성과 Dollar 객체의 생성과 리팩토링까지 수행했다.
이제 할 일 목록을 갱신해주자.
1. $5 + 10CHF = $10 (환율이 2:1인 경우) 2. $5 x 2 = $10 3. amount의 접근제어자를 private로 만들기 4. Dollar의 부작용 확인하기(side effect) 5. Money 반올림 확인하기
Plain Text
복사
할 일 목록 - 세 번쨰 업데이트
성공적으로 할 일 한 가지를 해결했다. 이제 이와 같은 과정으로 남은 할 일도 해결하면 된다.

2. Dollar 부작용(side effect)

이제 Dollar 객체의 부작용에 대해 생각해보자.
현재 Dollar 객체에는 몇 가지 문제가 있다.
Dollar five = new Dollar(5); five.times(2);
Java
복사
5라는 매개변수로 초기화한 five라는 Dollar객체는 times메서드를 통해 내부의 amount를 변경한다.
그럼 이 객체는 five라는 이름이 적절할까? 이 참조 변수명만 봐서는 다음 테스트도 통과해야 할 것 같다.
@Test void multiplyTest() { Dollar five = new Dollar(5); five.times(2); assertThat(five.amount).isEqualTo(10); five.times(3); assertThat(five.amount).isEqualTo(15); }
Java
복사
하지만, 당연하게도 이 테스트 케이스는 실패한다. five는 time(2)를 호출한 순간 5가 아닌 10이 되기 때문이다. 사실상 five가 아니라 ten이 되는 것이다.