Mockito - (1) 개요
by seonman.kim개요
개발을 테스트하려면 개발코드를 테스트하기 위해서 개발코드의 설정 코드를 작성해야 한다. 바로 아래와 같은 코드이다.
public class UserService {
private final UserRepository userRepository;
public User findByName(Name name) {
if(userRepository.validateUserName(name)) {
return userRepository.findByName(name);
}
return null;
}
}
public class UserRepository {
private List<User> list = new ArrayList<>();
public User findByName(Name name) {
for (User user : list) {
if (user.getName().equals(name)) {
return user;
}
}
return null;
}
public boolean validateUserName(Name name) {
if(name != null && name.getName() != null && 0 < name.getName().length()) {
return true;
}
return false;
}
public void create(User user) {
list.add(user);
}
}
- 서비스 코드와 레파지토리 코드
public class UserServiceTest {
@Test
public void findById(){
// given
Name name = new Name("SeonMan");
Age age = new Age(30);
User user = new User(name, age);
UserRepository repository = new UserRepository();
repository.create(user);
UserService service = new UserService(repository);
// when
User findUser = service.findByName(name);
// then
Assertions.assertThat(findUser).isEqualTo(findUser);
}
}
- 테스트 코드
테스트코드를 보면 불필요한 부분이 보인다.
- 데이터베이스에 값이 없으면 테스트는 실패한다.
- 테스트는 외부에 따라 결과가 달라지면 안 된다.
- 예제에서는 데이터베이스 대신 List 로 대신하였다.
- 테스트의 준비 코드를 작성해야 한다.
- 테스트를 위해 테스트 내용과 상관없이 객체를 생성하고 레파지토리에 값을 추가해야 한다. (1) 부분
위의 단점을 해소하기 위해 Mockito를 사용하여 보자.
설정 부분이다.
@RunWith(MockitoJUnitRunner.class)
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class) // 새로 추가된 부분
public class UserServiceTest {
@Test
public void findById(){
...
}
}
- JUnit 클래스에 Mockito관련 설정을 추가하기 위하여 @RunWith(MockitoJunitRunner.class) 를 추가한다.
- @RunWith를 사용 안 하면MockitoAnnotations.initMocks(this) 코드를 추가하여 Mock 을 초기화해주어야 한다.
- 스프링 부트 2.2부터 Mockito 는 'org.springframework.boot:spring-boot-starter-test' 라이브러리에 이미 추가되어 있다.
- 가독성 측면에서 추가하는 것이 좋을 듯.. 하다.
@Mock
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
@Mock
private UserRepository repository; // 새로 추가된 부분
@Test
public void findById(){
// given
Name name = new Name("SeonMan");
Age age = new Age(30);
User user = new User(name, age);
repository.create(user);
// UserRepository repository = new UserRepository(); // 삭제된 부분
// UserRepository repository = Mockito.mock(UserRepository.class) // 지역변수일경우
UserService service = new UserService(repository);
// when
User findUser = service.findByName(name);
// then
Assertions.assertThat(findUser).isEqualTo(findUser);
}
}
- 실제 객체가 아니라 임의로 조작 가능한 객체인 Mock 객체를 생성한다.
- 생성 방법
- 멤버 변수 : @Mock 사용한다.
- 지역변수 : Mockito.mock(UserRepository.class) 를 이용하여 생성한다.
@InjectMocks
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
@Mock
private UserRepository repository;
@InjectMocks // (1) 추가
private UserService service;
@Test
public void findById(){
// given
Name name = new Name("SeonMan");
Age age = new Age(30);
User user = new User(name, age);
repository.create(user);
// UserService service = new UserService(repository); // (2) 삭제
// when
User findUser = service.findByName(name);
// then
Assertions.assertThat(findUser).isEqualTo(findUser);
}
}
- Mock을 주입받는 객체를 만들어서 주입하지 않고 자동으로 Mock을 주입하도록 객체를 생성하고 추가해주는 어노테이션
- 생성자나 Setter 를 이용하여 자동으로 주입해준다.
Stub
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
@Mock
private UserRepository repository;
@InjectMocks
private UserService service;
@Test
public void findById(){
// given
Name name = new Name("SeonMan");
Age age = new Age(30);
User user = new User(name, age);
// when
Mockito.when(repository.validateUserName(name)).thenReturn(true); // 추가
Mockito.when(repository.findByName(name)).thenReturn(user); // 추가
User findUser = service.findByName(name);
// then
Assertions.assertThat(findUser).isEqualTo(findUser);
}
}
- 메소드의 실행 결과를 임의로 조작하여 설정 코드, 초기화 코드 필요 없이 테스트를 할 수 있도록 도와주는 기능
- when() 안에 내가 조작하기 원하는 메소드와 리턴되기를 원하는 객체를 기입하여 준다.
- repository가 가짜객체이기 때문에 repostiry.~~() 메소드는 Stub를 해주지 않는 이상 정상적으로 실행이 되지 않는다.
@Spy
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
@Spy
private UserRepository repository;
@InjectMocks
private UserService service;
@Test
public void findById(){
// given
Name name = new Name("SeonMan");
Age age = new Age(30);
User user = new User(name, age);
// when
// Mockito.when(repository.validateUserName(name)).thenReturn(true); // 삭제
Mockito.when(repository.findByName(name)).thenReturn(user);
User findUser = service.findByName(name);
// then
Assertions.assertThat(findUser).isEqualTo(findUser);
}
}
- @Spy를 사용하여 Stub을 하지 않은 메소드는 실제 메소드로 작동할 수 있도록 어노테이션을 변경한다.
최종
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
@Spy
private UserRepository repository;
@InjectMocks
private UserService service;
@Test
public void findById(){
// given
Name name = new Name("SeonMan");
Age age = new Age(30);
User user = new User(name, age);
// when
Mockito.when(repository.findByName(name)).thenReturn(user);
User findUser = service.findByName(name);
// then
Assertions.assertThat(findUser).isEqualTo(findUser);
}
}
최종 코드이다. 수정하면서 두 가지 문제점이 해결된 것을 볼 수 있다.
첫 번째는 Mock 객체를 만들어 레파지토리가 데이터베이스에서 조회하는 것이 아니라 Stub을 통하여 데이터베이스에 갔다 온 것처럼 메소드의 결과를 조작하였다. 그 결과 데이터베이스의 내용에 따라서 테스트의 결과가 달라지는게 아니라 항상 동일한 테스트 결과를 유지할 수 있었다.
두 번째는 Stub를 사용하여 메소드의 실행결과를 설정해주니 이로써 준비 코드 필요 없이 테스트를 진행할 수 있었다.
해당 메소드가 잘 작동하는 것을 테스트하려면 외부에서 해당 메소드의 영향을 끼치는 부분을 모두 통제하여야 올바른 테스트 결과를 얻어낼 수 있다. 우리는 이런 관점으로 Mockito 를 이해하고 바라봐야 할 것이다.
인용
Mockito @Mock @MockBean @Spy @SpyBean 차이점
Mockito 어노테이션(@Mock, @InjectMocks)
블로그의 정보
만선's 개발 블로그
seonman.kim