CS

(TDD) 테스트 주도 개발 정리 (1)

ri5 2022. 2. 3. 21:35

서론


TDD 스터디를 운영하면서 팀원들과의 책 내용을 보면서 따라하기보단 서로 개념을 나누면 좋을 것 같다는 생각에

이와 같이 블로그 글을 정리하는 것으로 더 오래 기억이 남게하기 위해 간단하게 정리를 한 글입니다.

본론


필자는 각각의 테스트를 목록화를 시키면서 다음과 같이 테스트를 진행을 하게 되는데 살펴보자면

  1. 테스트 코드 작성
  2. 테스트를 실행하고 구현한 코드가 실패하는지 확인
  3. 코드 작성
  4. 테스트 성공 확인
  5. 리팩토링을 통해 중복제거

필자는 아래의 과정을 통해 아래와 같은 점을 느낄 수 있다고 말합니다.

  • 각각의 테스트가 기능의 작은 변경에도 얼마나 커버가 되는지
  • 새 테스트를 동작하기 위해 얼마나 많은 하드 코딩을 하는지
  • 테스트를 얼마나 자주 실행하는지
  • 수없이 작은 단계를 거치면서 수많은 리팩토링을 합니다.

예를 들자면 Dollar는 객체와 Franc 이라는 객체를 추상 클래스를 통해 공통된 기능은 상속을 받아 사용함으로써 중복을 제거를 합니다. 코드로 예를 들자면

Test Code

달러와 프랑의 기능이 중복이 되면서 테스트 코드도 마찬가지로 중복이 되면서 작성을 하게 됩니다. 아래와 같은 샘플 코드를 여러 방식으로 중복을 제거할 수 있습니다.

public class ExchangeRateTest {
    @Test
    public void testMultiplication() {
        Dollar five = new Dollar(5);

        assertThat(five.times(2)).isEqualTo(new Dollar(10));
        assertThat(five.times(3)).isEqualTo(new Dollar(15));
    }

    @Test
    public void testEquality() {
        assertThat(new Dollar(5).equals(new Dollar(5))).isTrue();
        assertThat(new Dollar(5).equals(new Dollar(6))).isFalse();
        assertThat(new Franc(5).equals(new Franc(5))).isTrue();
        assertThat(new Franc(5).equals(new Franc(6))).isFalse();
        assertThat(new Franc(5).equals(new Dollar(5))).isFalse();
    }

    @Test
    public void testFrancMultiplication() {
        Franc franc = new Franc(5);

        assertThat(franc.times(2)).isEqualTo(new Franc(10));
        assertThat(franc.times(3)).isEqualTo(new Franc(15));
    }
}

Dollar Class

public class Dollar {

    private int amount;

    public Dollar(int amount){
        this.amount = amount;
    }

    public Dollar times(int multiplier) {
        return new Dollar(this.amount*multiplier);
    }

    public boolean equals(Object object) {
        Dollar dollar = (Dollar) object;
        return dollar.amount == this.amount;
    }
}

Franc Class

public class Franc {
    private int amount;

    public Franc(int amount){
        this.amount = amount;
    }

    public Franc times(int multiplier) {
        return new Franc(this.amount*multiplier);
    }


    public boolean equals(Object object) {
        Franc franc = (Franc) object;
        return franc.amount == this.amount;
    }
}

각각의 객체를 공통의 상위 클래스를 갖고 상속을 받게 됨으로써 중복을 제거하게 됩니다.

상속된 객체들

상속을 통한 중복 코드 제거


각 클래스들이 Money를 상속 받도록 합니다.

Money Class

public abstract class Money{

}
public class Franc extend Money{
    private int amount;

    :
    :
    :

public class Dollar extend Money{
    private int amount;

Dollar.equals(), Franc.equals()

서로 공통된 동작을 하도록 바꿔 줍니다.

public boolean equals(Object object) {
    Franc franc = (Franc) object;
    return franc.amount == this.amount;
}

public boolean equals(Object object) {
    Money money = (Money) object;
    return amount == money.amount;
}

Money Class

코드가 같아졌으니 상위 클래스인 Money 클래스로 올려줍니다. (중복 코드는 제거합니다.)

public abstract class Money{
    public boolean equals(Object object) {
        Money money = (Money) object;
        return amount == money.amount;
    }
}

여기서 전에 작성했던 테스트를 동작해보면 달러와 프랑이 같다고 나옵니다.

@Test
public void testEquality() {
    assertThat(new Dollar(5).equals(new Dollar(5))).isTrue();
    assertThat(new Dollar(5).equals(new Dollar(6))).isFalse();
    assertThat(new Franc(5).equals(new Franc(5))).isTrue();
    assertThat(new Franc(5).equals(new Franc(6))).isFalse();
    assertThat(new Franc(5).equals(new Dollar(5))).isFalse();
}

지저분 하지만 아래와 같이 클래스까지 비교를 합니다.

public abstract class Money{
    public boolean equals(Object object) {
        Money money = (Money) object;
        return amount == money.amount && getClass().equals(money.getClass());
    }
}

팩토리 메서드를 통한 중복 코드 제거


팩토리 메서드 패턴: 부모 클래스에 알려지지 않은 구체 클래스를 생성하는 패턴

TestCode

@Test
public void testMultiplication() {
    Dollar five = new Dollar(5);

    assertThat(five.times(2)).isEqualTo(new Dollar(10));
    assertThat(five.times(3)).isEqualTo(new Dollar(15));
}

                            :
                            :
                            :

@Test
public void testMultiplication() {
    Money five = Money.dollar(5);

    assertThat(five.times(2)).isEqualTo(Money.dollar(10));
    assertThat(five.times(3)).isEqualTo(Money.dollar(15));
}

이제 테스트에서 분리를 시키면서 모델이 변경되더라도 해당 팩토리 메서드만 수정을 하면 됩니다.

Money Class

public abstract class Money {
                    :
                    :
                    :

    static Money franc(int amount) {
        return new Franc(franc);
    }

    static Money dollar(int amount) {
        return new dollar(franc);
    }
}

경량화 패턴을 통한 중복 제거


중복된 정보를 가진 객체를 공유하여 메모리를 최적화합니다.

TestCode

@Test
public void testCurrency() {
    assertThat("USD").isEqualTo(Money.dollar(1).currency());
    assertThat("CHF").isEqualTo(Money.franc(1).currency());
}

Money.dollar().currency() 선언함으로 클래스가 아닌 currency()를 통해 통화를 확인 함으로써 경량화 시켰습니다.

Money

abstract String currency();

Dollar

통화를 리턴하는 메서드에서 통화를 지정해주는 생성자로 변경을 하여 의존성을 느슨하게 변경합니다.

public class Dollar extends Money{
  String currency(){
      return "USD";
  }

            :
            :
            :

public class Dollar extends Money{
    private String currency;

    public Dollar(int amount, String currency){
       this.amount = amount;
       this.currency = 
    }

Money

서로 동일한 생성자 형태이니 상위클래스로 이동 시킵니다. 이제 동일한 생성자를 제거함으로써 중복이 제거 되었습니다.

public Money(int amount, String currency){
    this.amount = amount;
    this.currency = 
};

Dollar

통화를 리턴하는 메서드에서 통화를 지정해주는 생성자로 변경을 하여 의존성을 느슨하게 변경합니다.

public class Dollar extends Money{
    private String currency;

    public Dollar(int amount, String currency){
       super(amount, currency);
    }

결론


TDD 테스트 주도 개발책을 13장까지 진행을하면서 생각보다 책을 읽으면서 샘플코드를 작성하는 것에 대해서는 어떻게 중복을 제거하고 TDD 사이클을 반복하는지 잘 이해를 할 수 없었습니다.
그래서 다시 정독하면서 블로그로 정리를 해보았고 각각의 상황에서 쓰이는 디자인 패턴과 서로 다른 코드를 똑같이 만드는 과정을 반복하고 중복을 제거하면서 한줄씩 밑줄이 그어지는 것을 보고 쾌감을 느끼는 저를 발견할 수 있었습니다.
이책에 쓰이는 내용이 실무에서 쓰인고 활용하기보다는 보다는 TDD가 어떻게 진행되고 리팩토링을 하는지에 대해 중점을 보고 진행하면 좋을 것 같습니다.