Lined Notebook

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);
    }
}
  • 테스트 코드

 

테스트코드를 보면 불필요한 부분이 보인다. 

  1. 데이터베이스에 값이 없으면 테스트는 실패한다. 
    • 테스트는 외부에 따라 결과가 달라지면 안 된다.
    • 예제에서는 데이터베이스 대신 List 로 대신하였다.
  2. 테스트의 준비 코드를 작성해야 한다.
    • 테스트를 위해 테스트 내용과 상관없이 객체를 생성하고 레파지토리에 값을 추가해야 한다. (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 @MockBean @Spy @SpyBean 차이점

예제 코드 https://github.com/cobiyu/MockitoSample Test Double이 왜 필요한 지부터 시작하는 기본적인 테스트 코드부터 한 단계씩 발전시켜나가며 Mockito의 어노테이션들의 정확한 쓰임새에 대해 살펴보겠습

cobbybb.tistory.com

Mockito 어노테이션(@Mock, @InjectMocks)

 

Mockito 어노테이션(@Mock, @InjectMocks)

Mockito 관련 어노테이션 @RunWith(MockitoJunitRunner.class) Mockito에서 제공하는 목객체를 사용하기 하기위해 위와같은 어노테이션을 테스트클래스에 달아준다. @RunWith(MockitoJunitRunner.class) public cl..

cornswrold.tistory.com

 

블로그의 정보

만선's 개발 블로그

seonman.kim

활동하기