코드에 SRP 원칙 적용 후 Mock 테스트 작성하기
SRP(Single Responsibility Principle) : 단일 책임 원칙
어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다. - 로버트 C.마틴
하나의 클래스에 역할과 책임이 너무 많은 경우 클래스를 각각의 역할과 책임에 따라 분리하자는 원칙이다.
SRP 원칙을 통해 클래스 하나가 과도한 책임을 가지는 것을 방지하고, 각 클래스의 책임이 명확해진다.
아래 글에 SOLID에 대해 정리했으니 시간나면 읽어보는 것도 좋다.
https://shout-to-my-mae.tistory.com/417
SRP 원칙에 따라 리팩토링하기
기존 코드
import java.util.Random;
public class Car {
private final String name;
private long position;
private final Random random = new Random();
public Car(String name) {
this.name = name;
this.position = 0;
}
public long moveMain(){
int randomValue = makeRandomValue();
return move(randomValue);
}
public int makeRandomValue(){
return random.nextInt(9);
}
public long move(int randomValue) {
if (randomValue >= 4) {
return ++position;
}
return position;
}
public void reset() {
this.position = 0;
}
}
자동차는 상태와 기본적인 행위를 가질 뿐만 아니라, 랜덤값을 생성하고 랜덤값에 따라 이동여부를 결정하는 역할까지 한다.
문제점
- 랜덤값 생성 로직이 변경되거나 자동차가 움직이는 방식이 달라질 경우 Car 클래스 코드를 변경해야한다.
- 랜덤 값을 생성하는 코드와 Car 객체를 움직이는 코드가 하나의 클래스에 있어 코드가 길어질 경우 가독성이 떨어진다.
SRP 원칙을 적용하여 책임 단위로 분리하기
- Car : 자동차 클래스는 자동차의 속성과 기본적인 동작만 담당한다.
- RandomValueGenerator : 랜덤 값을 생성한다.
- CarMover : 생성된 랜덤 값에 따라 자동차를 움직인다.
Car
public class Car {
private final String name;
private long position;
public long getPosition() {
return position;
}
public Car(String name) {
this.name = name;
this.position = 0;
}
public long move() {
return ++position;
}
public void reset() {
this.position = 0;
}
}
name은 당장 사용하지 않으므로 필요없어서 name에 대한 getter는 만들지 않았다.
(position 속성은 테스트에서 검증시 사용한다.)
RandomValueGenerator
public class RandomValueGenerator {
private final Random random = new Random();
public int generateRandomValue() {
return random.nextInt(9);
}
}
CarMover
public class CarMover {
private final RandomValueGenerator randomValueGenerator;
public CarMover(RandomValueGenerator randomValueGenerator) {
this.randomValueGenerator = randomValueGenerator;
}
public void move(Car car) {
int randomValue = randomValueGenerator.generateRandomValue();
if (randomValue >= 4) {
car.move();
}
}
}
CarMover에서 RandomValueGenerator를 생성자를 통해 의존성을 주입받도록 하였다.
- RandomValueGenerator 속성을 필드에서 바로 생성하여 사용해도 되지만, CarMover와 RandomValueGenerator간의 결합도가 높아지므로 의존성을 주입받았다.
또한 나중에 랜덤 값 생성 방식이 여러개로 확장될 경우 적절한 랜덤값 생성 방식을 선택하여 사용할 수 있도록 확장성을 고려했다. - private로 필드를 둔 이유는 private 접근 제어자를 통해 클래스 내부에서만 사용하여 캡슐화하기 위해서이다.
- 또한 필드에 final 키워드를 사용하면 오직 한번 할당되므로 값에 대한 예측이 쉽고 스레드 안정성이 높다.
SRP 원칙 적용으로 인한 파일의 개수 증가
책임에 따라 클래스를 분리하다보면 파일의 개수가 증가하여 관리가 어려울 수 있다.
그래서 적절한 기준을 가지고 클래스를 분리하는 것이 중요하다.
하지만 trade-off로 각 클래스의 책임이 명확해져 코드 유지보수가 쉽다.
그리고 책임 별로 클래스가 쪼개지므로 각각의 테스트 코드를 좀 더 쉽게 작성할 수 있어 좋다.
테스트 코드 리팩토링
기존 코드에서는 move 메서드에 랜덤값을 파라미터로 넣어줘서 자동차가 움직이는지 테스트할 수 있었다.
하지만 현재 코드에서는 랜덤 값 생성 객체(RandomValueGenerator)를 CarMover에 주입받아 움직이므로, 랜덤 값을 외부에서 컨트롤 할 수 없다.
해결 방법 : Stubbing
1. 랜덤 값 생성 객체(RandomValueGenerator)의 Mock 객체를 생성(@Mock)하고 CarMover에 주입(@InjectMocks)한다.
2. BDDMockito의 given()을 이용하여 CarMover에서 RandomValueGenerator의 랜덤 값 생성 메서드 호출 시 Mocking을 통해 가로채어 원하는 값로 반환하도록 변경한다.
코드
@DisplayName("움직이는 자동차 테스트")
class CarMoverTest {
private final Car car = new Car("Test car");
@Mock
private RandomValueGenerator randomValueGenerator;
@InjectMocks
private CarMover carMover;
private final Random random = new Random();
@BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
}
@AfterEach
void tearDown() {
car.reset();
}
@Test
@DisplayName("random 값이 4 이상 9 이하인 경우 1만큼 전진한다")
void moveCarWhenRandomValueIsGreaterThanOrEqual4() {
// given
int randomValue = 4 + random.nextInt(5);
given(randomValueGenerator.generateRandomValue()).willReturn(randomValue);
// when
carMover.move(car);
// then
assertThat(car.getPosition()).isEqualTo(1L);
}
@Test
@DisplayName("random 값이 3 이하인 경우 멈춘다")
void stopCarWhenRandomValueIsLessThan4() {
// given
int randomValue = random.nextInt(3);
given(randomValueGenerator.generateRandomValue()).willReturn(randomValue);
// when
carMover.move(car);
// then
assertThat(car.getPosition()).isEqualTo(0);
}
}
@Mock : Mock 객체(가짜 객체) 생성
@InjectMocks : Mock 객체를 해당 필드에 주입, 해당 필드의 의존성을 분석하여 해당 의존성에 대한 Mock 객체를 자동으로 주입한다.
✅ @Mock, @InjectMocks 사용시 해당 어노테이션 초기화하는 코드 @BeforeEach에 작성하기!
MockitoAnnotations.initMocks(this)