Recent Posts
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 달인막창
- PersistenceContext
- kotlin
- tolerated
- VARCHAR (1)
- 자원부족
- JanusWebRTC
- pytest
- JanusWebRTCGateway
- 티스토리챌린지
- Value too long for column
- vfr video
- 코루틴 빌더
- 개성국밥
- preemption #
- 깡돼후
- 오블완
- mp4fpsmod
- table not found
- JanusGateway
- Spring Batch
- python
- terminal
- 헥사고날아키텍처 #육각형아키텍처 #유스케이스
- JanusWebRTCServer
- k8s #kubernetes #쿠버네티스
- taint
- 코루틴 컨텍스트
- 겨울 부산
- PytestPluginManager
Archives
너와 나의 스토리
TDD(Test-Driven Development) 연습해보기 - 예제 1: Money (3) 12~17장 본문
개발/TDD(Test-Driven Development)
TDD(Test-Driven Development) 연습해보기 - 예제 1: Money (3) 12~17장
노는게제일좋아! 2021. 3. 11. 22:38반응형
"테스트 주도 개발 Test-Driven Development: By Example" 책에 나오는 예제를 실제로 구현해보자."
<현재까지 작성한 코드>
- Money.java
public class Money {
protected int amount;
protected String currency;
Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
Money times(int multiplier) {
return new Money(amount * multiplier, currency);
}
String currency() {
return currency;
}
static Money dollar(int amount) {
return new Money(amount, "USD");
}
static Money franc(int amount) {
return new Money(amount, "CHF");
}
public boolean equals(Object object) {
Money money = (Money) object;
return this.amount == money.amount &&
currency().equals(money.currency);
}
public String toString() {
return amount + " " + currency;
}
}
- MoneyTest.java
public class MoneyTest {
@Test
public void testMultiplication() {
Money five = Money.franc(5);
assertEquals(Money.franc(10), five.times(2));
assertEquals(Money.franc(15), five.times(3));
}
@Test
public void testEquality() {
assertTrue(Money.dollar(5).equals(Money.dollar(5)));
assertFalse(Money.franc(5).equals(Money.franc(6)));
assertFalse(Money.franc(5).equals(Money.dollar(5)));
}
@Test
public void testCurrency() {
assertEquals("USD", Money.dollar(1).currency());
assertEquals("CHF", Money.franc(1).currency());
}
}
"서로 다른 통화를 더해보자."
- $5 + 10CHF = $10(환율이 3:1인 경우)
- 위 기능을 구현하기 전에 간단한 작업부터 시도해보자.
- 일단 [$5 + $5 = $10]부터 시작해보자.
- 빨간 막대(오류)를 해결하기 위해 단순히 Money.dollar(10)를 리턴하는 plus() 메서드를 작성할 수 있다.
- 하지만, 어떻게 구현할지 명확하기 때문에 바로 제대로 구현을 하자.
- 다중 통화 연산을 어떻게 표현해야 할까?
- [(2 + 3) x 5]와 같은 연산부터 해보자.
- 우리의 경우엔 [($2 + 3CHF) x 5]가 되겠지만, 이렇게 하면 Money를 수식의 가장 작은 단위로 볼 수 있다.
- 그럼 테스트는 이런식으로 끝날 것이다.
@Test
public void testSimpleAddition() {
// ...
assertEquals(Money.dollar(10), reduced);
}
- reduced(축약된)란 이름의 Expression은 Expression에 환율을 함으로써 얻어진다.
- 실생활에서는 은행에서 환율이 적용되기 때문에 아래와 같이 쓸 수 있겠다.
@Test
public void testSimpleAddition() {
// ...
Money reduced = bank.reduce(sum, "USD");
assertEquals(Money.dollar(10), reduced);
}
- 왜 Bank가 reduce()를 수행할 책임을 맡았는가?
- Expression은 우리가 하려고 하는 일의 핵심이다.
- 핵심이 되는 객체가 다른 부분에 대해서 될 수 있는 한 모르도록 해야 한다.
- 그렇게 하면 핵심 객체가 가능한 오랫동안 유연할 수 있다. -> 테스트하기도 쉽고, 코드 재활용하기도 좋음
- Expression과 관련이 있는 연산이 많을 것 같다.
- 만약, 모든 연산을 Expression에만 추가한다면 Expression은 무한히 커질 것이다.
- 혹시 Bank가 별로 필요 없다는 reduce()를 구현할 책임을 Expression으로 옮겨도 좋다.
- Expression은 우리가 하려고 하는 일의 핵심이다.
- 테스트 코드를 작성해보자.
- 두 Money의 합은 Expression이어야 한다.
@Test
public void testSimpleAddition() {
// ...
Expression sum = five.plus(five);
Bank bank = new Bank();
Money reduced = bank.reduce(sum, "USD");
assertEquals(Money.dollar(10), reduced);
}
- $5를 만드는 건 간단하다. Money.dollar()를 호출하면 된다.
- 이걸 컴파일하도록 해보자.
- 먼저 Expression 인터페이스가 필요하다.
- Money.plus()는 Expression을 반환해야 한다.
- 즉, Money가 Expression을 구현해야 한다는 뜻이다.
- 이번에는 Bank 클래스가 필요하다.
- Bank에는 reduce()의 stub(스텁)이 필요하다.
- Test Stub: 테스트하는 동안 canned answer을 제공하는 것. 일반적으로 테스트를 위해 프로그래밍된 것 이외의 호출에는 반응하지 않는다.
- Canned answer: 정해진 질문에 대해 사전에 준비된 답
- 호출한 쪽에서 canned answer을 제공
- Test Stub: 테스트하는 동안 canned answer을 제공하는 것. 일반적으로 테스트를 위해 프로그래밍된 것 이외의 호출에는 반응하지 않는다.
- Bank에는 reduce()의 stub(스텁)이 필요하다.
- 이렇게 하면 컴파일은 되지만, 테스트는 실패한다.
- 그러면, 테스트가 통과하도록 간단하게 가짜 구현을 해보자.
- 이제 테스트를 성공적으로 통과한다.
"진짜로 만들기"
- 가짜 구현에 있는 $10는 테스트 코드에서 [ $5 + $5 ]와 같다.
- 이전에는 가짜 구현이 -> 진짜 구현으로 작업해 가는 것이 명확했다. 단순히 상수를 변수로 치환하는 일이었다.
- 그렇지만 이번에는 어떻게 작업할지 분명하지 않다. 그래서 일단은 순방향(진짜 구현 -> 가짜 구현)으로 작업해보자.
- 구현할 것: $5 +$5
- 우선 Money.plus()는 그냥 Money가 아닌 Expression(Sum)을 반환해야 한다.
- 두 Money의 합은 Sum이어야 한다.
- 용어 정리:
- augend: 덧셈의 첫 인자(피가산수)
- addend:가수
- 예: 1+2=3에서 가수는 2이고 피가수(augend)는 1이다.
- 이 테스트는 수행하고자 하는 연산의 외부 행위(더하기)가 아닌 내부 구현에 대해 너무 깊게 관여하고 있다.
- 하지만 이 테스트를 통과하면 목표에 한 걸음 더 다가가게 될 것이다.
- 이 코드를 컴파일시키기 위해 augend와 addend 필드를 가지는 Sum 클래스가 필요하다.
public class Sum {
Money augend;
Money addend;
}
- Money.plus()는 Sum을 리턴해야 한다.
// Money.java
Expression plus(Money addend) {
return new Sum(this, addend);
}
- Sum() 생성자도 필요하다.
// Sum.java
Sum(Money augend, Money addend){
}
- 그리고 Sum은 Expression의 일종이어야 한다.
- 이제 컴파일은 되니, 테스트가 통과하게 만들어보자.
- 우선 Sum 생성자에서 필드를 설정해줘야 한다.
// Sum.java
Sum(Money augend, Money addend) {
this.augend = augend;
this.addend = addend;
}
- 이제 테스트가 통과한다.
- 다음으로 넘어가자.
- Bank.reduce()는 Sum을 넘겨받고, 더한 두 통화(augend, addend)가 같다면 결과는 Sum 내에 있는 Money들의 amount를 합친 값을 갖는 Money 객체여야 한다.
- 물론 bank.reduce()에서 무조건 Money.dollar(10)를 리턴하므로 테스트는 실패한다.
- bank.reduce()를 고쳐보자.
// Bank.java
Money reduce(Expression source, String to) {
Sum sum = (Sum) source;
int amount = sum.augend.amount + sum.addend.amount;
return new Money(amount, to);
}
- 이 코드는 두 가지 이유로 지저분하다.
- 모든 Expression에 대해 캐스팅이 작동해야 한다.
- public 필드와 그 필드들에 대한 두 단계에 걸친 레퍼런스
- 이 문제들을 고쳐보자!
- 우선, 외부에서 접근 가능한 필드들을 들어내고 메서드 본문을 Sum으로 옮기자.
// Bank.java
Money reduce(Expression source, String to) {
Sum sum = (Sum) source;
return sum.reduce(to);
}
// Sum.java
public Money reduce(String to){
int amount = augend.amount+ addend.amount;
return new Money(amount, to);
}
- "Bank.reduce()의 인자로 Money를 전달하는 경우"에 대해 테스트 코드를 작성해보자.
@Test
public void testReduceMoney() {
Bank bank = new Bank();
Money result = bank.reduce(Money.dollar(1), "USD");
assertEquals(Money.dollar(1), result);
}
Money reduce(Expression source, String to) {
if (source instanceof Money) return (Money) source;
Sum sum = (Sum) source;
return sum.reduce(to);
}
- instanceof: 객체가 특정 클래스에 속하는지 아닌지를 확인.
- (Object) instanceof (Class Type) -> Object가 Class에 속하거나 Class를 상속받는 클래스에 속하면 true가 반환됨.
- 예:
Parent parent = new Child();
Child child = (Child) parent;
parent instanceof Child // true
- 코드가 지저분하쥬? 그래도 테스트는 통과한다 ㅎㅎ
- 클래스를 명시적으로 검사하는 코드가 있을 때에는 항상 다형성(polymorphism)을 사용하도록 바꾸는 것이 좋다.
- Sum은 reduce(String)을 구현하므로, Money에서도 그것을 구현하도록 만든다면 reduce()를 Expression 인터페이스에도 추가할 수 있게 된다.
- Sum, Money 둘 다 Expression을 구현하는 구조.
// Money.java
public Money reduce(String to) {
return this;
}
- 이러면, Bank.reduce()에서 불필요한 캐스팅 작업을 없앨 수 있다.
"프랑을 달러로 바꿔보자"
- [2프랑 = 1달러]라고 가정하자 ㅎㅎ
- 그렇다면 환율에 맞게 값 변화시키고 통화를 적용해야 한다.
// Money.java
public Money reduce(String to) {
int rate = (currency.equals("CHF") && to.equals("USD"))
? 2
: 1;
return new Money(amount / rate, to);
}
- 이렇게 하면, Money가 환율에 대해 알게 된다. 환율은 Bank만 알아야 하는데....
- 환율에 관한 것들은 Bank가 처리하도록 Expression.reduce()의 인자로 Bank를 넘겨주자.
// Bank.java
Money reduce(Expression source, String to) {
return source.reduce(this, to);
}
// Expression.java
public interface Expression {
Money reduce(Bank bank, String to);
}
// bank.java
public Money reduce(Bank bank, String to){
int amount = augend.amount+ addend.amount;
return new Money(amount, to);
}
- 인터페이스에 선언된 메서드 reduce()는 공용이므로 인자를 통일시켜야 한다.
// Money.java
public Money reduce(Bank bank, String to) {
int rate = (currency.equals("CHF") && to.equals("USD"))
? 2
: 1;
return new Money(amount / rate, to);
}
- 이제 bank가 환율을 처리하도록 하자.
// Money.java
public Money reduce(Bank bank, String to) {
int rate = bank.rate(currency, to);
return new Money(amount / rate, to);
}
- bank는 환율표를 가지고 있어야 한다.
- 두 개의 통화와 환율을 매핑시키는 해시 테이블을 사용할 수 있다.
- 통화 쌍을 해시 테이블의 키로 쓰기 위해 배열을 사용할 수 있을까?
- 테스트를 돌려 확인해보자.
- bank.rate() 때문에 컴파일 에러가 나겠지만, 대충 mocking이나 주석 처리하고 이 테스트부터 돌려보자.
- 테스트가 실패한다.
- 키를 위한 객체를 따로 만들어야겠다.
- 우린 이 Pair을 키로 쓸 거니까 equals()와 hashCode()를 구현해야 한다.
// Pair.java
public class Pair {
private String from;
private String to;
Pair(String from, String to) {
this.from = from;
this.to = to;
}
public boolean equals(Object object) {
Pair pair = (Pair) object;
return from.equals(pair.from) && to.equals(pair.to);
}
public int hashCode() {
return 0;
}
}
- hashCode를 0으로 두면 해시 테이블에서의 검색은 선형 검색과 비슷하게 수행될 것이다. But 일단은 대충 두자.
// Bank.java
public class Bank {
Money reduce(Expression source, String to) {
return source.reduce(this, to);
}
private Hashtable rates = new Hashtable<Pair, Integer>();
void addRate(String from, String to, int rate) {
rates.put(new Pair(from, to), rate);
}
int rate(String from, String to) {
Integer rate = (Integer) rates.get(new Pair(from, to));
return rate.intValue();
}
}
- 통화가 같을 때 rate는 1이 되어야 한다. 이를 테스트로 작성해보자.
- 테스트가 통과한다.
"서로 다른 통화를 더하자"
- 마지막으로 [$5 +10CHF]를 구현하자.
- 보다시피 컴파일 에러가 발생한다.
- 하나씩 고쳐가며 진행해보자.
@Test
public void testMixedAddition() {
Money fiveBucks = Money.dollar(5);
Money tenFrancs = Money.franc(10);
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Money result = bank.reduce(fiveBucks.plus(tenFrancs), "USD");
assertEquals(Money.dollar(10), result);
}
- Expression 대신 Money를 이용하니 컴파일 에러는 해결되었으나, 테스트가 실패한다.
- Sum.reduce()가 인자를 축약하지 않는 것 같아 보인다.
// Sum.java
public Money reduce(Bank bank, String to){
int amount = augend.reduce(bank, to).amount+ addend.reduce(bank, to).amount;
return new Money(amount, to);
}
- 이렇게 두 인자를 모두 축약했더니 테스트가 통과했다.
- 하지만 Money 대신 Expression을 써야하므로, Money들을 조금씩 없애보자.
- 예를 들어 피가산수와 가수는 이제 Expression으로 취급할 수 있다.
- Expression에 plus()가 정의되지 않았다.
- 이제 테스트 통과!!
추상화
- To do list:
- Sum.plus() 테스트 코드 작성
- Sum.plus() 구현
- Expression.times() 테스트 코드 작성
- Expression.times() 구현
- Sum.plus() 테스트 코드 작성
- fiveBucks와 tenFrances를 더해서 Sum을 생성할 수도 있지만, 명시적으로 Sum을 생성해보자.
@Test
public void testSumPlusMoney(){
Expression fiveBucks = Money.dollar(5);
Expression tenFrancs = Money.franc(10);
Bank bank = new Bank();
bank.addRate("CHF","USD",2);
Expression sum = new Sum(fiveBucks, tenFrancs).plus(fiveBucks);
Money result = bank.reduce(sum, "USD");
assertEquals(Money.dollar(15), result);
}
- Sum.plus() 구현
// Sum.java
public Expression plus(Expression addend) {
return new Sum(this, addend);
}
- Expression.times() 테스트 코드 작성
@Test
public void testSumTimes(){
Expression fiveBucks = Money.dollar(5);
Expression tenFrancs = Money.franc(10);
Bank bank = new Bank();
bank.addRate("CHF", "USD",2);
Expression sum = new Sum(fiveBucks, tenFrancs).times(2);
Money result = bank.reduce(sum, "USD");
assertEquals(Money.dollar(20), result);
}
- Expression.times() 구현
// Expression.java
Expression times(int multiplier);
// Sum.java
public Expression times(int multiplier) {
return new Sum(augend.times(multiplier), addend.times(multiplier));
}
// Money.java
public Expression times(int multiplier) {
return new Money(amount * multiplier, currency);
}
- 테스트 통과!!
출처:
- [테스트 주도 개발 Test-Driven Development: By Example"]
반응형
'개발 > TDD(Test-Driven Development)' 카테고리의 다른 글
TDD(Test-Driven Development) 테스트 주도 개발 패턴 정리 (2) | 2021.03.31 |
---|---|
TDD(Test-Driven Development) 연습해보기 - 예제 1: Money (2) 9~11장 하위 클래스 제거하기 (0) | 2021.02.25 |
TDD(Test-Driven Development) 연습해보기 - 예제 1: Money (1) 1~8장 (0) | 2021.02.25 |
Comments