SSAFY11th-book-study / book-study

SSAFY 11기 6반의 '토비의 스프링 스터디'
0 stars 0 forks source link

[2.4.2] 테스트와 인터페이스 #17

Closed hj-k66 closed 7 months ago

hj-k66 commented 8 months ago

이거는 질문은 아닌데, 책에서는 효율적인 테스트를 위해서라도 인터페이스를 두고 DI를 적용한다고 합니다. 또, 테스트하기 어려운 코드를 인터페이스 DI를 통해 테스트가 가능하게 바꿀 수도 있고요.

이와 관련해서 연습해보면 좋을 예제가 있어서 다들 고민해보면 좋을 것 같아 올립니다!

Q. 테스트하기 어려운 코드를 테스트 가능한 구조로 변경하기

다음 Car 객체의 move() 메소드의 이동/정지를 테스트하고 싶은데 테스트하기 힘들다. move() 메소드 내에 random 값이 생성되고 있기 때문이다. move() 메소드를 테스트 하기 위해선 어떻게 해야할까? 단, random 값을 메소드의 인자로 전달해 해결하면 안 된다. 인터페이스를 활용해 해결해보자.

public class Car {
    private static final int FORWARD_NUM = 4;
    private static final int MAX_BOUND = 10;

    private int position = 0;

    public void move() {
        if (getRandomNo() >= FORWARD_NUM)
            this.position++;
    }

    private int getRandomNo() {
        Random random = new Random();
        return random.nextInt(MAX_BOUND);
    }
}
gmelon commented 8 months ago
상혁
Car에서 이동 여부를 판단하는 로직을 별도의 클래스로 분리해볼 수 있을 것 같습니다. 이를 위해 먼저 MovingStrategy 인터페이스를 만듭니다 ```java @FunctionalInterface public interface MovingStrategy { boolean movable(); } ``` 그리고 프로덕션 환경에서는 랜덤 값 구현체를 사용해 랜덤 값에 따라 이동 여부를 결정하도록 합니다. ```java class RandomMovingStrategy implements MovingStrategy { private static final int FORWARD_NUM = 4; private static final int MAX_BOUND = 10; @Override public boolean movable(int number) { return getRandomNumber() >= FORWARD_NUM; } private int getRandomNumber() { Random random = new Random(); return random.nextInt(MAX_BOUND); } ``` * Client ```java public class Car { private int position = 0; public void move(MovingStrategy strategy) { if(strategy.movable()) { this.position++; } } } ``` 이렇게 하면 테스트에서는 항상 이동하도록 / 혹은 항상 이동하지 않도록 하는 MovingStrategy의 구현체를 DI 해주어 랜덤 값에 의존하지 않는 테스트를 수행할 수 있습니다. (MovingStrategy이 함수형 인터페이스이기 때문에 구현체를 직접 만들지 않고 람다식도 사용 가능합니다) * Test ```java Car car = new Car(); @Test public void isMoved() { // 이동시키는 경우 car.move(() -> true)); // 이동시키지 않는 경우 car.move(() -> false)); // 이동 여부에 따른 검증 수행 ... } ``` 이렇게 했을 때의 장점은 테스트가 어려운 영역을 최대한 격리할 수 있는 것이라고 생각합니다. Car의 경우 다른 로직을 테스트할 때 방해가 되는 랜덤 값 생성 로직을 별도의 객체로 분리해내고 원하는 로직의 검증에 집중할 수 있습니다.
limjongheok commented 8 months ago
종혁
> 우선 저 같은 경우 경험으로 테스트 할 기능이 테스트하기 어려운 기능을 종속할 경우 어려운 기능을 인터페이스로 빼고 구현하여 테스트 할 클래스에 의존성 주입을 하여 사용했던 것 같습니다. 기능 분리 후 구현 ```java public interface MoveAble { boolean moveAble(); } public class Move implements MoveAble { private static final int FORWARD_NUM = 4; private static final int MAX_BOUND = 10; @Override public boolean moveAble() { if (getRandomNo() >= FORWARD_NUM) return true; return false; } private int getRandomNo() { Random random = new Random(); return random.nextInt(MAX_BOUND); } } ``` Car 클래스 ```java public class Car { private final MoveAble moveAble; private int position = 0; public Car(MoveAble moveAble) { this.moveAble = moveAble; } public int getPosition() { return position; } public void move(){ if(moveAble.moveAble()) this.position++; } } ``` test는 위 상혁님이 설명을 잘해 주셔서 저는 mock 테스트로 예시를 들면 ```java @ExtendWith(MockitoExtension.class) class CarTest { @InjectMocks private Car car; @Mock private Move move; ``` 다음과 같이 주입하는 개체인 Move 클래스를 mock화시켜 놓고 ```java @Test @DisplayName("자동차 움직일때 테스트") public void 자동차_움직일때_테스트(){ //given given(move.moveAble()).willReturn(true); // when car.move(); //then Assertions.assertThat(car.getPosition()).isEqualTo(1); } ``` 다음과 같이 해당 값이 true가 나오도록 기대를 시킨 후 주입받는 car 클래스를 테스트 할 경우 ![image](https://github.com/SSAFY11th-book-study/book-study/assets/42922673/f08aa496-1a15-42df-a12f-20418c8352da) 테스트를 통과 하는 모습을 볼 수 있습니다. 다음은 테스트가 통과하지 않는 모습 ```java @Test @DisplayName("자동차 움직일때 테스트2") public void 자동차_움직일때_테스트2(){ //given given(move.moveAble()).willReturn(true); // when car.move(); //then Assertions.assertThat(car.getPosition()).isEqualTo(0); } ``` ![image](https://github.com/SSAFY11th-book-study/book-study/assets/42922673/dc48ce4b-3e21-4a3f-97e8-e4010054f621) 이처럼 DI를 통하면 기존 테스트하기 어려운 기능도 mock을 통해 쉽게 테스트할 수 있습니다.
a-young-kim commented 8 months ago
김아영
```java public interface CarMove { boolean isMove(); } ``` ```java public class MovingStrategy implements CarMove{ private static final int FORWARD_NUM = 4; private static final int MAX_BOUND = 10; private int getRandomNo() { Random random = new Random(); return random.nextInt(MAX_BOUND); } @Override public boolean isMove() { return getRandomNo() >= FORWARD_NUM; } } ``` ```java public class Car { private int position; public void move(CarMove moving){ if(moving.isMove()) position++; } public int getPosition() { return position; } } ``` ```java class CarTest { @Test void test1() { Car car = new Car(); int cnt = car.getPosition(); car.move(() -> true); assertThat(car.getPosition()).isEqualTo(cnt + 1); } @Test void test2() { Car car = new Car(); int cnt = car.getPosition(); car.move(() -> false); assertThat(car.getPosition()).isEqualTo(cnt); } } ```
hj-k66 commented 7 months ago
희정
변경이 필요한 부분을 랜덤번호를 생성하는 getRandomNo()이라 생각하고 해당 부분을 인터페이스로 DI하게 했습니다. 실제 로직에서는 RandomStrategy클래스를 구현하여 원래 로직대로 랜덤 숫자를 생성해 move()판단 조건으로 여기고, 테스트 시에는 람다식을 이용해 비교할 숫자를 임의로 만들어 넘겨주었습니다. ## Car 클래스 ```java public class Car{ private static final int FORWARD_NUM = 4; private static final int MAX_BOUND = 10; private int position = 0; public void move(MoveStrategy moveStrategy) { if(moveStrategy.getNum() >= FORWARD_NUM) this.position++; } public int getPosition(){ return position; } } ``` ## MoveStrategy 인터페이스 ```java public interface MoveStrategy { int getNum(); } ``` ## RandomStrategy 클래스 ```java public class RandomStrategy implements MoveStrategy{ private static final int MAX_BOUND = 10; @Override public int getNum() { Random random = new Random(); return random.nextInt(MAX_BOUND); } } ``` ## CarTest ```java public class CarTest { private Car car; @BeforeEach void setUp(){ car = new Car(); } @Test @DisplayName("move기준이 4 이상이면 position 증가") void move(){ car.move(() -> 4); assertThat(car.getPosition()).isEqualTo(1); car.move(() -> 10); assertThat(car.getPosition()).isEqualTo(2); } @Test @DisplayName("move기준이 3 이하이면 전진하지 않음") void notMove(){ car.move(() -> 3); assertThat(car.getPosition()).isEqualTo(0); } } ```
sootudio commented 7 months ago
수영
random number를 만드는 부분을 인터페이스로 만들었습니다. - 인터페이스 생성 ```java public interface RandomBehavior { int getRandomNo(); } ``` ```java import java.util.Random; public class RandomGenerator implements RandomBehavior { private static final int MAX_BOUND = 10; @Override public int getRandomNo() { Random random = new Random(); return random.nextInt(MAX_BOUND); } } ``` ```java public class FixedValueGenerator implements RandomBehavior { private int fixedValue; public FixedValueGenerator(int fixedValue) { this.fixedValue = fixedValue; } @Override public int getRandomNo() { return fixedValue; } } ``` - Car 클래스 ```java public class Car { private static final int FORWARD_NUM = 4; private int position = 0; private RandomBehavior randomBehavior; public Car(RandomBehavior randomBehavior) { this.randomBehavior = randomBehavior; } public void move() { if (randomBehavior.getRandomNo() >= FORWARD_NUM) this.position++; } } ``` - 테스트 코드 ```java import org.junit.Test; import static org.junit.Assert.assertEquals; public class CarTest { @Test public void testMoveWithFixedValueGenerator() { // Given RandomBehavior fixedValueGenerator = new FixedValueGenerator(5); Car car = new Car(fixedValueGenerator); int initialPosition = car.getPosition(); // When car.move(); // Then assertEquals(initialPosition + 1, car.getPosition()); } } ```