이거는 질문은 아닌데, 책에서는 효율적인 테스트를 위해서라도 인터페이스를 두고 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);
}
}
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의 경우 다른 로직을 테스트할 때 방해가 되는 랜덤 값 생성 로직을 별도의 객체로 분리해내고 원하는 로직의 검증에 집중할 수 있습니다.
> 우선 저 같은 경우 경험으로 테스트 할 기능이 테스트하기 어려운 기능을 종속할 경우 어려운 기능을 인터페이스로 빼고 구현하여 테스트 할 클래스에 의존성 주입을 하여 사용했던 것 같습니다.
기능 분리 후 구현
```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을 통해 쉽게 테스트할 수 있습니다.
```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);
}
}
```
변경이 필요한 부분을 랜덤번호를 생성하는 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);
}
}
```
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());
}
}
```
이거는 질문은 아닌데, 책에서는 효율적인 테스트를 위해서라도 인터페이스를 두고 DI를 적용한다고 합니다. 또, 테스트하기 어려운 코드를 인터페이스 DI를 통해 테스트가 가능하게 바꿀 수도 있고요.
이와 관련해서 연습해보면 좋을 예제가 있어서 다들 고민해보면 좋을 것 같아 올립니다!
Q. 테스트하기 어려운 코드를 테스트 가능한 구조로 변경하기
다음 Car 객체의 move() 메소드의 이동/정지를 테스트하고 싶은데 테스트하기 힘들다. move() 메소드 내에 random 값이 생성되고 있기 때문이다. move() 메소드를 테스트 하기 위해선 어떻게 해야할까? 단, random 값을 메소드의 인자로 전달해 해결하면 안 된다. 인터페이스를 활용해 해결해보자.