[Clean Code & TDD] -3.2 TDD : Test 하기 어려운 코드

2021. 12. 9. 15:22카테고리 없음

TDD에서는 Test 가 언제 어디서 실행하든지,

항상 Pass 해야한다.

(어느 때는 실패, 어느 때는 성공하는 case는 안좋은 test code 이다.)

 

기존에 Car 객체는 move()를 할때 랜덤 난수를 생성해서 전진 혹은 후퇴를 결정하는데,

Test 할때마다 랜덤값을 보장하지 못하여서 밑의 테스트 코드는 항상 통과하지 못한다.

( 기준과 다른 난수가 생성되면 반대의 행위가 일어날 수 있다)

public class CarTest {
	@Test
    void 이동() {
		Car car = new Car("pobi");
        car.move();
        assertThat(car.getPosition()).isEqualTo(1);
    }
    
    @Test
    void 정지() {
    	Car car = new Car("pobi");
        car.move();
        assertThat(car.getPosition()).isEqualTo(0);
    }
}
public class Car {
	...
    
    public void move() {
    	if (getRandomNo() >= FORWARD_NUM) {
        	this.position++;
        }
    }
    
    private int getRandomNo() {
    	return Random().nextInt(MAX_BOUND);
    }
}

 

위와 같은 코드를 항상 테스트를 통과하게 하려면 어떻게 리팩토링 해야할까?

 

방법은 여러가지다

 

1. private 으로 설정된 getRandomNo() 를 protected 로 바꾸고,

     테스트 코드에서 익명클래스를 활용하여 @Override 해주어 정적인 값을 테스트 코드 내에서 리턴하게 하여 

    성공을 확실하게 정한다.

public class CarTest {
	@Test
    void 이동() {
    	Car car = new Car("pobi") {
        	@Override
            protected int getRandomNo() {
            	return 4;
            }
        };
        
        car.move();
        assertThat(car.getPosition()).isEqualTo(1);
    }
    ...
}

 

2. move() 메서드에 인자를 받아서 random 난수 생성의 역할을 외부로 분리시킨다.

-> 이렇게 하면 테스트 코드에서는 move(3); 과 같이 인자를 직접 넣어서 테스트 성공을 확실하게 정할 수 있다.

 

3. 움직임의 여부를 결정하는 로직을 외부 인터페이스로 추출한다.

-> 확장성을 고려한 설계 & 구현

(개인적으로는 이 방법이 가장 유용하다고 생각한다.)

public class Car {
	...
    public void move(MovingStrategy movingStrategy) {
    	if (movingStrategy.moveable()) {
        	this.position++;
        }
    }
    ...
}

 

public interface MovingStrategy {
	boolean moveable();
}
public class CarTest {

	...
    
    @Test
    void 이동() {
    	Car car = new Car("pobi");
        car.move(() -> true);
        assertThat(car.getPosition()).isEqualTo(1);
    }
    ...
}

이처럼 interface를 생성하여 구현하면 다양한 요구사항에 대처할 수 있게된다.