본문 바로가기
  • Where there is a will there is a way.
개발/clean code

테스트 주도 개발

by 소확행개발자 2019. 8. 9.

테스트 주도 개발

켄트백 아저씨

 

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

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

3. 조금 수정한다.

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

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

 

 

테스트 주도 개발이기 때문에 무턱대고 우선 시나리오 대로 코드를 작성한다.

@RunWith(SpringJUnit4ClassRunner.class)
public class DollarTests {

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

}

 

여기서 

1. 객체가 없을 것이고

2. 생성자가 없을 것이고

3. 메서드가 없을 것이다.

 

 

package com.waug.cube.member.v1.doller;

public class Dollar {

    public int amount;

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

    public void times(int multiple){
       this.amount = amount * multiple;
    }

}

 

그래서 이렇게 직접 생성해준다.

 

이렇게 되면 테스트 코드가 성공 할것이다.

 

우리의 목적은 작동하는 깔끔한 코드를 얻는 것이다.

 

위처럼 작성하면 

 

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

 

해당 코드는 실패할 것이다. 왜냐하면 five 의 amount 밸류가 변경되었기 때문이다.

 

이 문제를 해결하려면 리턴될때 새로운 객체를 만들어야 한다.

 

package com.waug.cube.member.v1.doller;

public class Dollar {

    public int amount;

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

    public Dollar times(int multiple){
        return new Dollar(amount * multiple);
    }

}

 

위와 같이 코드가 작성되면

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

    }

다음과 같이 코드가 수정 될 것이다.

 

여기다가 equals 를 붙이게 되면

 

public class Dollar {

    private int amount;

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

    public Dollar times(int multiple){
        return new Dollar(amount * multiple);
    }

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

}

 

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

    }

가 된다.

 

 

그렇담 달러 테스트 코드가 작성되었고 우선 동일하게 프랑스 화폐 테스트 코드를 작성해보자

 

package com.home.exchange;

public class Franc {

    private int amount;

    public Franc(int amount) {

        this.amount = amount;
    }


    public Franc times(int multiple) {

        return new Franc(amount * multiple);
    }

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

Franc 도 테스트 코드 작성이 되었다.

하지만 현재는 Dollar 와 모두 중복되는 코드이다.

 

그렇다면 자바에서 공통 상위 클래스를 사용해보자

 

Dollar - Franc => Money

 

package com.home.exchange;

public class Franc extends Money {


    public Franc(int amount) {

        this.amount = amount;
    }


    public Franc times(int multiple) {

        return new Franc(amount * multiple);
    }

}
package com.home.exchange;

public class Money {

    protected int amount;

    @Override
    public boolean equals(Object object){

        Money money = (Money) object;
        return money.amount == amount;

    }

}

 

 

 

다음과 같이 수정이 될 것이다.

하지만 여기서 문제가 발생한다.

 

Dollar 가 결국엔 Franc 이 되어버린다. equals 부분에서 

 

public class Money {

    protected int amount;

    @Override
    public boolean equals(Object object){

        Money money = (Money) object;
        return money.amount == amount && getClass().equals(money.getClass());

    }

}

 

이렇게 바꿔 주었다.

 

그리고 나서 보면 times 도 많이 닮았다는 것을 알게 된다.

 

여기서 Money 를 factory method 방법으로 빼면 어떨까

 

package com.home.exchange;

public class Money {

    protected int amount;

    @Override
    public boolean equals(Object object){

        Money money = (Money) object;
        return money.amount == amount && getClass().equals(money.getClass());

    }

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

}

 

package com.home.exchange;

public abstract class Money {

    protected int amount;

    @Override
    public boolean equals(Object object){

        Money money = (Money) object;
        return money.amount == amount && getClass().equals(money.getClass());

    }

    public static Dollar dollar(int amount){
        return new Dollar(amount);
    }

    public abstract Money times(int multiple);

}

 

우선 Money 를 abstarct 로 만들고 times 를 공통 메소드로 뺄 수 있다.

 

package com.home.exchange;

public class Dollar extends Money {

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

    @Override
    public Money times(int multiple) {

        return new Dollar(amount * multiple);
    }

}

 

그렇게 되면 다음과 같이 Dollar 가 정리된다.

 

이렇게 되면 Franc 와 Dollar 는 times 가 공통되게 된다.

 

다음과 같이 Money 를 선언해서 사용하게 되면 

 

@Test
    public void testMultiplication(){

        Money five = Money.dollar(2);
        assertEquals(Money.dollar(10), five.times(5));
        assertFalse(Money.dollar(10).equals(Money.franc(10)));

    }

다음과 같은 테스트 코드가 가능해지고

다음과 같이 테스트 코드를 작성하게 되면 

클라이언트가 Dollar 라는 이름의 하위 클래스가 있다는 사실을 모르게 된다.

 

이제는 통화의 개념을 도입해보자 

 

package com.home.exchange;

public class Franc extends Money {

    private String currency;

    public Franc(int amount) {

        this.amount = amount;
        this.currency = "CHF";
    }


    @Override
    public Money times(int multiple) {

        return new Franc(amount * multiple);
    }

    public String currency(){
        return currency;
    }

}
package com.home.exchange;

public class Dollar extends Money {

    private String currency;

    public Dollar(int amount){

        this.amount = amount;
        this.currency = "USD";
    }

    @Override
    public Money times(int multiple) {

        return new Dollar(amount * multiple);
    }

}

 

다음처럼 우선 테스트 코드 작성을 위해 설계할 수 있겠다.

 

여기서 다시 리펙하자면

 

Dollar 부분에 amount 와 currency 부분이 중복이라 위로 올릴 수가 있다.

 

package com.home.exchange;

public class Franc extends Money {


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


    @Override
    public Money times(int multiple) {

        return Money.franc(amount * multiple);
    }

    @Override
    public String currency(){
        return currency;
    }

}

 

package com.home.exchange;

public abstract class Money {

    protected int amount;
    protected String currency;

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

    @Override
    public boolean equals(Object object){

        Money money = (Money) object;
        return money.amount == amount && getClass().equals(money.getClass());

    }

    public static Dollar dollar(int amount){
        return new Dollar(amount, "USD");
    }
    public static Franc franc(int amount) { return new Franc(amount, "CHF"); }

    public abstract Money times(int multiple);
    public abstract String currency();

}

 

이 된다.

 

이제 남은건 times 이다. 

하지만 

Money.franc

Money.dollar 

를 어떻게 빼낼까?

 

다시 돌아가서 

 

new Franc()

new Money() 로 생각해보자

 

package com.home.exchange;

public class Franc extends Money {


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


    @Override
    public Money times(int multiple) {

        return new Franc(amount * multiple, "CHF");
    }

    @Override
    public String currency(){
        return currency;
    }

}

와 같이 만들 수 있다.

 

여기서 CHF 는 변하는가 ? Franc 일 경우 변하지 않는다 따라서

 

package com.home.exchange;

public class Franc extends Money {


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


    @Override
    public Money times(int multiple) {

        return new Franc(amount * multiple, currency);
    }

    @Override
    public String currency(){
        return currency;
    }

}

 

로 완성할 수 있다.

 

자 그렇다면 Franc 이라고 굳이 써야할 이유가 있을까?

 

없다 따라서 Franc 을 Money 로 바꿀 수 있다. 

package com.home.exchange;

public class Franc extends Money {


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


    @Override
    public Money times(int multiple) {

        return new Money(amount * multiple, currency);
    }

    @Override
    public String currency(){
        return currency;
    }

}

이렇게 되면 Money 부분을 뺄 수 있다.

 

아 지금보니 currency 부분도 뺄 수 있겠다. 

 

package com.home.exchange;

public class Franc extends Money {


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

}

 

Money 부분은 abstract 클래스에서 new 선언이 안되기 때문에

abstarct 를 뺏다.

 

package com.home.exchange;

public class Money {

    protected int amount;
    protected String currency;

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

    @Override
    public boolean equals(Object object){

        Money money = (Money) object;
        return money.amount == amount && currency().equals(money.currency());

    }

    public Money times(int multiple) {

        return new Money(amount * multiple, currency);
    }



    public static Dollar dollar(int amount){
        return new Dollar(amount, "USD");
    }
    public static Franc franc(int amount) { return new Franc(amount, "CHF"); }


    public String currency(){
        return currency;
    }


}

이렇게 설정되고 보면 

하위 클래스들이 하는 역할을 super 밖에 없게 된다. 즉 의미가 없다

 

그러면 Money 클래스는 다음과 같이 수정되게 된다.

 

package com.home.exchange;

public class Money {

    protected int amount;
    protected String currency;

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

    @Override
    public boolean equals(Object object){

        Money money = (Money) object;
        return money.amount == amount && currency().equals(money.currency());

    }

    public Money times(int multiple) {

        return new Money(amount * multiple, currency);
    }



    public static Money dollar(int amount){
        return new Money(amount, "USD");
    }
    public static Money franc(int amount) { return new Money(amount, "CHF"); }


    public String currency(){
        return currency;
    }


}

 

테스트 코드도 간단하게 수정 될 수 있다.

 

import com.home.exchange.Money;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import org.junit.Test;

public class MultiCurrencyTests {


    @Test
    public void testMultiplication(){

        Money five = Money.dollar(2);
        assertEquals(Money.dollar(10), five.times(5));
        assertFalse(Money.dollar(10).equals(Money.franc(10)));

    }


}

 

 

'개발 > clean code' 카테고리의 다른 글

나쁜 코드로 치르는 대가  (0) 2019.02.13
테스트 주도 개발  (0) 2019.02.11

댓글