본문 바로가기

TDD, CleanCode

[TDD]테스트 주도 개발 예제(Money) - KentBeck

 

https://github.com/jiwon3346/Test-Driven-Development_kentBeck

 

jiwon3346/Test-Driven-Development_kentBeck

Contribute to jiwon3346/Test-Driven-Development_kentBeck development by creating an account on GitHub.

github.com

※ 테스트 주기

1. 작은 테스트를 하나 추가한다.

2. 모든 테스트를 실행해서 테스트가 실패하는 것을 확인한다.

3. 조금 수정한다.

4. 모든 테스트를 실행해서 테스트가 설공하는 것을 확인한다.

5. 중복을 제거하기 위해 리팩토링을 한다.

 

※ 배운 것

1. 일단 컴파일 시키자

    - 이를 위해 null을 리턴한다던지, 일단 상수로 리턴한 후에 차차 변수로 일반화하는 과정을 가진다. (이를 가짜로 구        현하기, 스텁 구현 등으로 표현한다.)

 

2. 새로운 할일이 생기면 딴 것들을 모두 제쳐두고 일단 할일 목록에 추가부터 하자

 

3. 값을 객체로 만드는 것을 염두해 두자.

    @Test
    public void testMultiplication(){
        Dollar five = new Dollar(5);
        assertEquals(10, five.times(2));
    }

해당 코드는 객체를 한번밖에 사용 할 수 없다.

Class Dollar{
	Dollar times(int multiplier){
	return new Dollar(amount * multiplier);
	}
}
    
    @Test
    public void testMultiplication(){
        Dollar five = new Dollar(5);
        Dollar product = five.times(2);
        assertEquals(10, product.amount);
        product = five.times(3);
        assertEquals(15, product.amount);
    }

 

위와 같이 int형이 아닌 새로운 Dollar객체를 리턴하게 되면, 값은 변경되는 것이 아니라서 언제든지 원하는 값으로 생성된다.

 

4. equals과 hasCode의 오버라이딩을 활용하자

    - 3과 같은 경우가 발생시에 거의 필수적이다. 항상 값을 비교할 수단을 만들자

 

5. 삼각 측량법을 사용하여 테스트 하자

    - 삼각 측량법은 다음과 같이 두가지 경우에 모두 테스트를 통과시키도록 하는 방법이다.

    @Test
    public void testEquality(){
        assertTrue(new Dollar(5).equals(new Dollar(5)));
        assertFalse(new Dollar(5).equals(new Dollar(6)));
    }

True 예제에서는 return 값으로 True(상수)만을 리턴해도 상관없지만

False 예제를 만족하지 못한다.

사실 간단한 답이지만 다음과 같이 문제를 해결한다.

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

 

6. equals 오버라이딩을 하면 assertEquals 역시 적용된다.

    - 확실하진 않으나 아직까진 그렇다. 

    @Test
    public void testMultiplication(){
        Dollar five = new Dollar(5);
        assertEquals(new Dollar(10), five.times(2));
        assertEquals(new Dollar(15), five.times(3));
    }

이는 정상적으로 작동된다.

 

7. 중복을 제거하라

    - 서로 다른 두 클래스의 중복을 제거하기 위해, 상속할 부모 클래스를 생성했다.

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

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

Dollar 객체와 Franc 객체의 equals는 동일한 코드처럼 보인다. Money객체를 만들어서 상속하면 두개의 코드 모두 제거 할 수 있다.

public class Money {
    protected int amount;

    @Override
    public boolean equals(Object object){
        Money money = (Money) object;
        return money.amount == this.amount 
        && getClass().equals(money.getClass());
    }
}

참고로 getClass 메소드는 함수로 사용할 때는 해당 클래스의 하위 클래스를 인스턴스에 메소드로 사용할 땐 해당 인스턴스의 하위 클래스를 반환한다.  

 

8. 하위 클래스를 팩토리 메서드를 이용하여 선언하고, 하위 클래스의 역할을 줄여보자. (어쩌면 하위 클래스를 없앨 수 있다.)

    @Test
    public void testMultiplication(){
        //Dollar five = new Dollar(5);
        Money five = Money.dollar(5);
 
 public abstract class Money{
    static Dollar dollar(int amount) {
        return new Dollar(amount);
    }
}

해당 주석을 다음과 같이 Money객체의 메서드로 생성할 수 있다. (이런 방식이면 Dollar객체를 완전히 숨길 수 있다.)

 

9. 상위 클래스와 하위클래스가 있을 경우 하위 클래스부터 만들며 수정하라

public abstract class Money {
    protected int amount;
    protected String currency;

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

    static Dollar dollar(int amount) {
        return new Dollar(amount, "USD");
    }

    static Franc franc(int amount) {
        return new Franc(amount, "CHF");
    }

    String currency(){
           return currency;
    }
}

currency 라고 불리는 문자열 통화 변수를 추가했다. 위의 코드는 결과물이지만 처음부터 저렇게 만들어 졌지 않았다. 

 

class Franc{
	String currency() {
    	return "CHF"
    }
}

class Dollar{
	String currency() {
    	return "USD"
    }
}

이는 다시 리팩토링 된다.

class Franc{
	private String currency;
    Franc(int amount){
    	this.amount = amount;
        currency = "CHF";
    }
}
	String currency() {
    	return currency
    }
}

Dollar 역시 동일하다.

 

만약 Money.franc 메소드에서 인자로 전달 할 수 있다면, 그리고 그 이전에 생성자에서 문자열을 받을 수 있다면, 공통 구현이 가능해 진다.

 

class Franc{
    private String currency;
    Franc(int amount, String currency){
    	this.amount = amount;
        this.currency = currency;
    }
}
	String currency() {
    	return currency
    }
}

10. 9 이후에 하위 클래스의 중복을 제거 하라.

부모 클래스와 자식 클래스 모두 같은 변수나 메소드가 있다면, 충분한 오해의 소지를 만들고, null과 같은 부정확한 답을 리턴하는 경우가 생긴다.

public class Money {
    protected int amount;
    protected String currency;

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

    String currency(){
        return currency;
    }

    @Override
    public boolean equals(Object object){
        Money money = (Money) object;
        return money.amount == this.amount && currency().equals(money.currency());
    }
}

public class Franc extends Money {

    String currency;

    Franc(int amount, String currency){
        super(amount,currency);
    }

    Money times(int multiplier){
        return new Money(amount * multiplier,currency);
    }

    String currency(){
        return currency;
    }
}​

Money는 equals를 실행시킬 때, currency() 값으로 Money의 currency 대신 오버라이딩 되어버린 Franc에 currency를 가져오게 된다.

그런데 생성자에서 super로 넘어오게 되고 이는 다시 Money로 넘어가기 때문에 코드가 꼬이게 되고 null이 출력되는 것 같다. (왜 그런지 사실 잘 모르겠다.)

디버깅 해보면 알 수 있다. super는 하위 클래스의 내용을 모두 상위 클래스에 넘긴다. 그렇기 때문에 Franc의 amount와 currency는 처음에는 값이 있지만, Money로 넘어 가면서 null값으로 초기화된다.

이 떄문에 다음에 Franc에서 메소드를 불러오면(currency나 times) 이 때는 Franc에서 찾으면 null이 나오게 된다( Money에서 찾아야 한다)

 

11. 테스트는 쌓아두지 않아도 된다.

    - 도중에 필요 없어진 테스트 과한 테스트는 제거해도 좋다. 다만 신중하게 생각하자.