본문 바로가기

IT Book Summary/TDD

13-17장

13장 진짜로 만들기

 

$5 + 10CHF = $10 (환율이 2:1 일 경우)

$5 + $5 = $10

데이터 중복도 제거하자.

 

public void testSimpleAddition() {
    Money five = Money.dollor(5);
    Expression sum = five.plus(five);
    Bank bank = new Bank();
    Money reduced = bank.reduce(sum, "USD");
    assertEquals(Money.dollar(10), reduced);
}

Bank
Money reduce (Expression source, String to) {
    return Money.dollar(10);
}

 

이전에는 상수를 변수로 치환하며 가짜구현을 진짜 구현으로 거꾸로 작업해나가는 것이 명확.

이번에는 순방향으로 작업해보자.

$5 + $5 에서 Money 반환하기

 

Money.plus()는 그냥 Money 가 아닌 Expression(Sum) 을 반환해야 함

 

 

public void testPlusReturnSum() {
    Money five = Money.dollar(5);
    Expression result = five.plus(five);
    Sum sum = (Sum) result;
    assertEquals(five, sum.augend); //덧셈의 첫인자. 피가산수 augend 
    assertEquals(five, sum.addend);
}
// 이 테스트는 연산의 외부행위가 아닌 내부 구현에 대해 너무 깊에 관여

 

컴파일을 위해 augend와 addend 필드를 가지는 Sum 클래스가 필요 

 

Money으 plus()가 반환하는 Sum생성자도 필요

 

Sum 클래스가 Expression 구현하게 만들어줌

 

Sum 생성자 필드값도 같이 설정해주자.

Money 
Expression plus(Money addend) {
    return new Sum(this, addend);
}

Sum
class Sum implements Expression {
    Money augend;
    Money addend;

    Sum(Money augend, Money addend) {
        this.augend = augend;
        this.addend = addend;
    }
}

 

Sum의 Money 통화가 모두 동일하고, reduce의 반환 Money 통화 역시 같다면 

-> 결과는 Sum 내에 있는 Money들의 amount를 합친값을 갖는 Money객체

 

public void testReduceSum() {
    Expression sum = new Sum(Money.dollar(3), Money.dollar(4));
    Bank bank = new Bank();
    Money result = bank.reduce(sum, "USD");
    assertEquals(Money.dollar(7), result);
}

 

Sum에서 계산결과 - 결과는 Money,  두 Money의 합, 통화는 우리가 축약하는 통화

 

Bank
Money reduce(Expression source, String to) {
    Sum sum = (Sum) source;
    int amount = sum.augend.amount + sum.addend.amount;
    return new Money(amount, to);
}
// 다음의 두가지 이유로 리팩토링 되어야함
// - 캐스팅. 모든 Expression
// - 공용필드와 그 필드들에 대한 두 단계 걸친 레퍼런스

Bank
Money reduce(Expression source, String to) {
    Sum sum = (Sum) source;
    return sum.reduce(to);
}

Sum
public Money reduce(String to) {
    int amount = augend.amount + addend.amount;
    return new Money(amount, to);
}

 

테스트 항목 추가

Bank.reduce(Money)

일단 테스트 작성

public void testReduceMoney() {
    Bank bank = new Bank();
    Money result = bank.reduce(Money.doller(1), "USD");
    assertEquals(Money.dollar(1), result);
}

Bank
Money reduce(Expression source, String to) {
    if(source instanceof Money) return (Money) source;
    Sum sum = (Sum) source;
    return sum.reduce(to);
}

 

Sum은 reduce(String)를 구현하므로 Money 도 구현하게 한후

Expression 인터페이스에 추가

 

이후 Bank의 캐스팅과 클래스 검사코드를 지워주자.

 

Expression과 Bank에 이름은 동일하나 매개변수가 다른 메서드가 있는것이 거슬림.

항목에 추가해두자.

 

 

  • 모든 중복이 제거되기 전까지 테스트 통과한 것이 아님
  • 구현을 위해 역방향이 아닌 순방향으로 작업
  • 필요할 것으로 예상되는 객체(Sum)의 생성을 위한 테스트 작성. testPlusReturnSum()
  • 일단 빠르게 구현 (Sum생성자)
  • 캐스팅을 이용해 한곳에 코드를 한곳에 구현후, 일단 테스트 돌리고 적당한 자리배치
  • 다형성을 사용해 명시적 클래스 검사를 제거

 


14장. 바꾸기

 

Money에 대한 통화 변환을 수행하는 Reduce

2프랑을 달러로 바꾸고싶다.

 

public void testRuduceMoneyDifferentCurrency() {
    Bank bank = new Bank();
    bank.addRate("CHF", "USD", 2);
    Money.result = bank.reduce(Money.franc(2), "USD");
    assertEquals(Money.dollar(1), result);
}

Money
public Money reduce(String to) {
    int rate = (currency.equals("CHF") && to.equals("USD"))
        ? 2
        :1;
    return new Money(amount/rate, to);
}

 

환율에 대한 일은 모두 Bank가 처리해야 한다.

 

Bank
int rate (String from, String to) {
    return (from.equals("CHF") && to.equals("USD"))
    ? 2
    : 1;
}

Money
public Money reduce(Bank bank, String to) {
    int rate = bank.rate(currency, to);
    return new Money(amount / rate, to);
}

 

상수를 없애려면 Bank에서 환율표를 가지고 있다가 참고할수 있게 해야함

두개의 통화와 환율을 매핑시키는 해시테이블 사용하자

 

 

public void testArrayEquals() {
    assertEquals(new Object[] {"abc"}, new Object[] {"abc"});
}
// 테스트 실패..
// 키를 위한 객체를 따로 만들자

Pair
private class Pair{
    private String from;
    private String to;
    
    Pair(String from, String to) {
        this.from = from;
        this.to = to;
    }
    
    public boolean equals() {
        Pair pair = (Pair) object;
        return from.equals(pair.from) && to.equals(pair.to);
    }
    
    public int hashCode() {
        return 0;
    }
}

 

0은 최악의 해시코드지만 일단 구현해놓자.

 

환율을 저장하는 무언가 필요하다. 그리고 환율을 설정할수 있고 환율을 얻어낼수도 있게 하자.

 

Bank
private Hashtable rates = new Hashtable();

void addRate(String from, String to, int rate) {
    rates.put(new Pair(from, to), new Integer(rate));
}

int rate(String from, String to) {
    Integer rate = (Integer) rates.get(new Pair(from, to));
    return rate.intValue();
}

 

같은 환율을 요청하면 1이 되어야 한다.

 

public void testIdentityRate() {
    assertEquals(1, new Bank().rate("USD", "USD"));
}

Bank
int rate(String from, String to) {
    if(from.equals(to)) return 1; // 추가해준 후 테스트가 성공한다.
    Integer rate = (Integer) rates.get(new Pair(from, to));
    return rate.intValue();
}

 

 

  • 필요할거라고 생각하나 인자를 빠르게 추가
  • 코드와 테스트 사이에 있는 데이터 중복을 끌어냄
  • 자바 오퍼레이션에 대한 가정 검사 테스트 작성(testArrayEquals)
  • 테스트없이 전용 도우미 클래스 만듦
  • 리팩토링중 실수가 있었으나 문제를 분리하기위해 다른 하나의 테스트를 작성하며 전진

 


15장. 서로 다른 통화 더하기

 

$5 + 10CHF = $10(환율이 2:1일 경우)

드디어 메인항목 관련 테스트 추가

 

public void testMixedAddition() {
    Expression fiveBucks= Money.dollar(5);
    Expression 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);
}

 

컴파일은 안되지만 천천히 가자.

 

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);
}

 

테스트 실패

Sum.reduce() 가 인자를 축약하지 않아서.

 

Sum
public Money reduce(Bank bank, String to) {
    int amount= augend.reduce(bank, to).amount 
                     + addend.reduce(bank,to).amount;
    return new Money(amount, to);
}
 

 

테스트 통과.

 

이제 피가산수와 가수, 인자 또한 Expression으로 취급

 

Sum
Expression augend; 
Expression addend;
 
Sum(Expression augend, Expression addend) {
    this.augend= augend; 
    this.addend= addend;  
}

 

Money plus()인자 와 times() 반환값도 Expression 취급

 

 

 Money
 Expression plus(Expression addend) {
     return new Sum(this, addend);
 }
 
 Expression times(int multiplier) {
     return new Money(amount * multiplier, currency);
 }

 

테스트 케이스의 plus() 인자도 바꾸자

 

public void testMixedAddition() {
    Expression fiveBucks= Money.dollar(5);
    Expression 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 plus()정의 

 

Expression
Expression plus(Expression addend); // 정의

Money
public Expression plus(Expression addend) { // 공용
    return new Sum(this, addend);
}

Sum 
public Expression plus(Expression addend) {
    return null; // 스텁구현
}

 

  • 원하는 테스트 작성, 한단계 뒤로..
  • 추상적인 선언을 통해 일반화
  • 변경후(Expression fiveBucks), 영향받은 다른 부분 변경.

16장. 드디어 추상화

 

$5+$5 에서 Money 반환

Sum.plus
Expression.times

 

 

//Sum.plus()에 대한 테스트
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을 생성하는것이 의도를 더 드러냄

 

Sum
public Expression plus(Expression addend) {
    return new Sum(this, addend);
}

 

Tdd로 구현할때 테스트 코드의 줄 수와 모델코드 줄수가 거의 비슷한 상태로 끝난다.

경제적으로 하기 위해서는 TDD가 자신의 방법에 비해 어떻게 다른지 측정 필요.

 

Expression.times()선언

 

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);
}

 

 

Money.times()와 Sum.times() 가시성 높여줌

 

Expression
Expression times(int multiplier);
 
Sum
public Expression times(int multiplier) {
    return new Sum(augend.times(multiplier), addend.times(multiplier));
}
 
Money
public Expression times(int multiplier) {
    return new Money(amount * multiplier, currency);
}

 

이제 테스트 통과

 

public void testPlusSameCurrencyReturnsMoney() {
    Expression sum= Money.dollar(1).plus(Money.dollar(1));
    assertTrue(sum instanceof Money);
}

//테스트를 통과하기 위해 다음처럼 구현
Money
public Expression plus(Expression addend) {
    return new Sum(this, addend);
}

 

  • 미래 코드 읽을 사람을 위해 테스트 작성
  • TDD 와 현재 개발스타일 비교하는 방법 제시
  • 선언부 수정이 나머지부분을 고치게 함
  • 실험을 시도했고 실패함.

17장. Money 회고

 

  • 다음 할일은 무엇인가

지저분한 중복이 남앗다.

또 다른 질문은 어떤 테스트들이 추가로 더 필요할까? 이다.

실패하거나 성공하는 테스트에 이유를 찾자

마지막으로 설계한것을 검토하자.

 

 

  • 메타포 - 설계구조에 엄청난 영향

Money 예제의 결과가 기존에 했던것과 많이 다르다.

수식 메타포를 생각했는다 다른방향으로 흘렀다.

새로 프로그램을 작성한다면 매번 새로운 통찰을 얻고 놀라움을 경험할까?

 

  • JUnit사용도

조급하게 리팩토링을 한 경우 일분에 한번정도 테스트를 실행했다.

테스트 사이 시간간격에 대한 히스토그램 참고

 

  • 코드 메트릭스

코드에 대한 통계

실제코드와 테스트 코드에 담긴 

클래스, 함수, 줄, 회기성 복잡도, 함수당 줄수에 대한 통계를 내 분석했다.

 

  • 프로세스

TDD 주기는 다음과 같다.

  • 작은 테스트 추가
  • 모든 테스트 실행, 실패하는것 확인
  • 코드에 변화를 준다
  • 모든 테스트를 실행하고, 성공하는것 확인
  • 중복을 제거하고 리팩토링

Money 테스트에 대한 수정 횟수의 히스토그램

테스트를 통화하기위한 스텁을 생성하면 수정횟수는 줄어들수 있다.

 

 

  • 테스트의 질

TDD 부산물로 자연히 생기는 테스트들은 함께 유지돼야 할만큼 유용하다.

하지만 이 테스트들은 다른종류의 테스트를 대체할수는 없을것이다.

  • 성능테스트
  • 스트레스 테스트
  • 사용성 테스트

전문테스팅의 역할은

시스템이 무엇을 해야 하는지에 대해 일반적으로 어떤 느낌을 갖고 있느 사람들과

시스템이 실제로 그 일을 하도록 만들 사람들 간의 의사소통을 증폭시켜주는 무언가로 바뀐다.

 

- 명령문 커버리지는 테스트 질에 대한 평가기준은 아니지만 시작점이 될수 있다.

- 결함삽입은 테스트의 직을 평가하는 또 다른 방법이다. 

 

테스트 커버리지를 향상시키는 방법은 더 많은 테스트를 작성하는 것

또는 테스트의 수를 그대로 두면서 프로그램 로직을 단순화 하는것이다.

 

  • 최종검토

- 테스트를 확실히 돌아가게 만드는 세가지 접근법:

가짜로 구현하기, 삼각측량법, 명백하게 구현하기

- 설계를 주도하기 위한 방법으로 테스트 코드와 실제 코드 사이의 중복 제거

- 페이스를 조절하듯 테스트 사이 간격을 조절할 수 있는 능력

 

 

 

 

 

 

 

 

'IT Book Summary > TDD' 카테고리의 다른 글

xUnit 21장 - 24장  (0) 2020.03.31
18장-20장 xUnit  (0) 2020.03.24
10-12장  (0) 2020.03.10
7-10장  (0) 2020.03.09
4-6장  (0) 2020.03.02