[Test] 테스트 주도 개발 (11 ~ 14장)

DaeHoon·2022년 10월 26일
0

TDD

목록 보기
3/9

11. 모든 악의 근원

TestFrancMultiplication 제거

  static Money dollar(int amount){
    return new Money(amount, "USD");
  }
  static Money franc(int amount){
    return new Money(amount, "CHF");
  }
  • 팩토리 매서드의 리턴을 상위 클래스로 변경하면서 하위 클래스에 대한 참조를 줄였다.
  • 이렇게 되면 Dollar에 대한 참조는 다 사라진다, 하지만 여전히 Franc에 대한 참조는 있는 상태

Franc를 참조하는 테스트 코드 제거

  @Test
  public void testEquality(){
    assertTrue(Money.dollar(5).equals(Money.dollar(5)));
    assertFalse(Money.dollar(5).equals(Money.dollar(6)));
    assertFalse(Money.franc(5).equals(Money.dollar(5)));
  }
  • Franc를 참조하는 testDifferentClassEquality 테스트와 동치성 테스트인 testEquality에 있는 단언 몇 개를 제거했다.

우리는

  • 하위 클래스의 속을 들어내는 걸 완료하고, 하위 클래스를 삭제했다.
  • 기존의 소스 구조에서는 필요했지만 새로운 구조에서는 필요 없게 된 테스트를 제거했다.

12. 드디어, 더하기

  @Test
  public void testSimpleAddition(){
    Money sum = Money.dollar(5).plus(Money.dollar(5));
    assertEquals(Money.dollar(10), sum);
  }
  • 이러한 간단한 테스트 코드를 작성하고
  Money plus(Money addend){
    return new Money(amount + addend.amount, currency);
  }
  • Money 클래스에 plus 메서드를 만들어주자.

다중 통화 연산을 어떻게 표현할 것인가?

  • Money와 비슷하게 동작하지만 사실은 두 Money의 합을 나타내는 객체를 만든다.
  • 이를 구현하기 위해 Money의 합을 마치 지갑처럼 취급해보자. 한 지갑에는 금액과 통화가 다른 여러 화폐들이 들어갈 수 있다.
  • 또는 (2+3) * 5 같은 수식이다. 우리 경우에는 ($2 + 3CHF)x5가 되겠지만, 이렇게 하면 Money를 수식의 가장 작은 단위로 볼 수 있다.
  • 연산의 결과로 Expression들이 생기는데, 그 중 하나는 합이 될 것이다. 연산(포트폴리오의 값을 합산하는 것 등)이 완료되면, 환율을 이용해서 결과 Expression을 단일 통화로 축약시킬 수 있다.
  @Test
  public void testSimpleAddition(){
    Bank bank = new Bank();
    Money reduced = bank.reduce(sum, "USD");
    assertEquals(Money.dollar(10), reduced);
  }
  • reduced란 이름의 식은 식에 환율을 적용함으로써 얻어진다. 환율이 적용되는 곳은 은행이므로 위와 같이 테스트 코드를 작성했다.

완성된 테스트 코드

  @Test
  public void testSimpleAddition(){
    Money five = Money.dollar(5);
    Expression sum = five.plus(five);
    Bank bank = new Bank();
    Money reduced = bank.reduce(sum, "USD");
    assertEquals(Money.dollar(10), reduced);
  }
  • 두 Money의 합은 Expression 이어야 한다.
  • $5는 팩토리 메서드로 만들었다.

객체 생성

public class Bank {
  Money reduce(Expression source, String to){
    return null;
  }
}

public interface Expression {
}

public class Money implements Expression{

	...
  Expression plus(Money addend){
    return new Money(amount + addend.amount, currency);
  }
}

  • Money.plus()는 Expression을 구현해야 한다. 일단 인터페이스를 구현하게끔 Money 코드를 수정하자.
  • bank에는 reduce의 스텁(소프트웨어 개발에 쓰이고 다른 프로그래밍 기능을 대리하는 코드)이 있어야 한다.
  • 컴파일이 되고 실패한다. 간단히 reduce에 10 달러를 리턴하게끔 수정해보자
  Money reduce(Expression source, String to){
    return Money.dollar(10);
  }

우리는

  • 큰 테스트를 작은 테스트 ($5 + 10CHF에서 $5 + $5로) 줄여서 발전을 나타낼 수 있도록 했다.
  • 우리에게 필요한 계산에 대한 가능한 메타포들을 신중히 생각해봤다.
  • 새 메타포에 기반하여 기존의 테스트를 재작성했다.
  • 테스트를 빠르게 컴파일 했다.
  • 그리고 테스트를 실행했다.
    -진짜 구현을 만들기 위해 필요한 리팩토링을 하였다.

13. 진짜로 만들기

만들어보자

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

  
   @Test
  public void testSimpleAddition(){
    Money five = Money.dollar(5);
    Expression sum = five.plus(five);
    Bank bank = new Bank();
    Money reduced = bank.reduce(sum, "USD");
    assertEquals(Money.dollar(10), reduced);
  }
  • 코드 중복은 없지만 데이터 중복이 있다. Bank의 10 달러와, 테스트 코드에 있는 five.plus(five) 부분이 같다.
  @Test
  public void testSimpleAddition(){
    Money five = Money.dollar(5);
    Expression result = five.plus(five);
    Sum sum = (Sum) result;
    assertEquals(five, sum.augend);
    assertEquals(five, sum.addend);
  }
  • 우선 Money.plus()는 그냥 Money가 아닌 Expression(Sum)을 반환해야 한다. 즉 두 Money의 합은 Sum이여야 한다.
  • 이 테스트를 통과하기 위해 Sum을 구현해보자
public class Sum implements Expression{
  Money augend;
  Money addend;

  Sum(Money augend, Money addend){
    this.augend = augend;
    this.addend = addend;
  }
}
  • Sum을 구현했다.
  Expression plus(Money addend){
    return new Sum(this, addend);
  }
  • Sum을 구현하고 Money를 반환하는 Expression.plus도 수정하자.
  • 이제 Bank.reduce()는 Sum을 전달받는다. 만약 Sum이 가지고 있는 Money의 통화가 모두 동일하고, reduce를 통해 얻어내고자 하는 Money 통화도 같으면 결과는 Sum 내에 있는 Money들의 amount를 합친 값을 갖는 Money 객체여야 한다.
  @Test
  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);
  }
  • 위와 같이 작성할 수 있다. 이제 10달러만 반환하게 임시로 만들어 둔 Bank.reduce()를 수정해보자.
Bank
  Money reduce(Expression source, String to){
    Sum sum = (Sum) source;
    return sum.reduce(to);
  }
  • Sum에서 reduce를 구현하지 않아서 컴파일이 안된다. 마찬가지로 구현해주자
Sum
  public Money reduce(String to){
    int amount = augend.amount + addend.amount;
    return new Money(amount, to);
  }
  • Sum에서 구현했다.

추가로 테스트 작성

  @Test
  public void testReduceMoney(){
    Bank bank = new Bank();
    Money result = bank.reduce(Money.dollar(1), "USD");
    assertEquals(Money.dollar(1), result);
  }
  • 막대가 초록색이고 위의 코드에 대해 더 할 것이 명확하지 않으니 추가로 테스트를 작성하자.
  • ClassCastException이 발생하면서 빨간 막대 상태가 된다.

코드 수정

public interface Expression {
  Money reduce(String to);
}
  • Expression 인터페이스에 reduce 메서드를 추가한다.
Money
  @Override
  public Money reduce(String to) {
    return this;
  }
  
Bank
  Money reduce(Expression source, String to){
    return source.reduce(to);
  }
  • Money에 reduce를 오버라이딩하여 구현한 다음, Bank의 reduce에서 source의 reduce를 반환하게 수정한다.

우리는

  • 모든 중복이 제거되기 전까지는 테스트를 통과한 것으로 치치 않았다.
  • 구현하기 위해 역방향이 아닌 순방향으로 작업했다.
  • 앞으로 필요할 것으로 예상되는 객체(Sum)의 생성을 강요하기 위한 테스트를 작성했다.
  • 빠른 속도로 구현하기 시작했다. (Sum의 생성자)
  • 일단 한 곳에 캐스팅을 이용해서 코드를 구현했다가, 테스트가 돌아가자 그 코드를 적당한 자리로 옮겼다.
  • 명시적인 클래스 검사를 제거하기 위해 다형성을 사용햇다.

14. 바꾸기

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

  @Test
  public void testReduceMoneyDifferentCurrency(){
    Bank bank = new Bank();
    bank.addRate("CHF", "USD", 2);
    Money result = bank.reduce(Money.franc(2), "USD");
    assertEquals(Money.dollar(1), result);
  }
  • 통화 변경을 구현하기 위해 위와 같은 테스트를 작성하자
  @Override
  public Money reduce(String to) {
    int rate = (currency.equals("CHF") && to.equals("USD")) ? 2: 1;
    return new Money(amount / rate, to);
  }
  • 그 후 테스트를 통과시키기 위해 Money에 환율을 하드코딩 하여 작성했다.
  • 환율에 대한 것은 모두 Bank 가 처리해야 한다 Expression에 인자로 Bank를 주도록 수정하자.
public interface Expression {
  Money reduce(Bank bank, String to);
}

public class Bank {
  Money reduce(Expression source, String to){
    return source.reduce(this, to);
  }
}

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

public class Money{
...
  @Override
  public Money reduce(Bank bank, String to) {
    int rate = (currency.equals("CHF") && to.equals("USD")) ? 2: 1;
    return new Money(amount / rate, to);
  }
}
  • Expression에 Bank 인자를 추가하면서 위와 같이 코드를 수정했다.
Bank
  public int rate(String from, String to){
    return (from.equals("CHF") && to.equals("USD")) ? 2: 1;
  }
 
Money 
@Override
  public Money reduce(Bank bank, String to) {
    int rate = bank.rate(currency, to);
    return new Money(amount / rate, to);
  }
  • Bank에 환율에 관한 메서드를 추가하고 Money.reduce()에서 bank.rate()를 호출해 환율에 대한 코드를 제거했다.
  • 하지만 여전히 2 (환율)이 하드코딩 되어있는 상태다.

환율표

  • 하드코딩을 제거하기 위해 두 개의 통화와 환율을 매핑시키는 환율표를 해시 테이블로 구현을 할 것이다.
  @Test
  public void testArrayEquals(){
    assertEquals(new Object[] {"abc"}, new Object[] {"abs"});
  }
  • 테스트는 당연히 실패한다. 두 객체는 동일한 객체가 아니기 때문이다

해시 테이블 키

public class Pair {
  private String from;
  private String to;
  
  Pair(String from, String to){
    this.from = from;
    this.to = to;
  }
  
  public int hashCode(){
    return 0;
  }
}
  • 먼저 해시 테이블의 키 값이 되는 Pair 객체를 만들어준다.
  • 일단 hashCode는 0으로 설정해주자.
Bank
  private Hashtable rates = new Hashtable();

  public void addRate(String from, String to, int rate){
    rates.put(new Pair(from, to), new Integer(rate));
  }
  
  
  public int rate(String from, String to){
    if (from.equals(to))
      return 1;
    Integer rate = (Integer) rates.get(new Pair(from, to));
    return rate.intValue();
  }
  • Bank 클래스에서 해시 테이블을 선언하고 addRate 메서드를 정의하준다. rate는 환율이다.
  • 그리고 rate 함수에서 해시 테이블을 통해 환율을 얻을 수 있게 코드를 수정해준다.
  @Test
  public void testIdentityRate(){
    assertEquals(1, new Bank().rate("USD", "USD"));
  }
  • 초록 막대!

우리는

  • 필요할 거라고 생각한 인자를 빠르게 추가했다. (rate)
  • 코드와 테스트 사이에 있는 데이터 중복을 끄집어냈다.
  • 자바의 오퍼레이션에 대한 가정을 검사해보기 위한 테스트(testArray-Equals)를 작성했다.
  • 별도의 테스트 없이 전용 도우미 클래스를 만들었다.
  • 리팩토링하다가 실수를 했고, 그 문제를 분리하기 위해 또 하나의 테스트를 작성하면서 계속 전진했다.
profile
평범한 백엔드 개발자

0개의 댓글