본문 바로가기
Spring/개념

Spring Test Mock 사용법 및 특징

by clearinging 2021. 10. 30.
반응형

예제 코드

링크

Mockito 종류

Mockito.mock(T)
@Mock
@MockBean
@Spy
@SpyBean
@InjectMocks

예제 코드

Entity

@Getter
@Setter
@NoArgsConstructor
public class Entity {
    private Long id;
    private String name;

    @Builder
    public Entity(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}
  • 목적 : 저장 목적을 가지고 있는 객체

EintityIdCreator

@Component
public class EntityIdCreator {
    private long id = 0;

    public long generateNewId() {
        long generateId = id;
        id++;
        return generateId;
    }
}
  • 목적 : Entity의 Id를 생성하기 위한 Component

TestRepository

@Repository
public class TestRepository {
    private static final Map<Long, Entity> db = new HashMap<>();

    public Entity save(Entity saveCandidate) {
        db.put(saveCandidate.getId(), saveCandidate);
        return saveCandidate;
    }

    public List<Entity> findAll() {
        System.out.println("spy test");

        Set<Map.Entry<Long, Entity>> entries = db.entrySet();

        return entries.stream()
                .map(Map.Entry::getValue)
                .collect(Collectors.toList());
    }

    public Entity findById(Long id) {
        return db.get(id);
    }
}
  • 목적 : Entity 를 저장/조회 하는 것

TestService

@Service
@RequiredArgsConstructor
public class TestService {
    private final TestRepository testRepository;
    private final EntityIdCreator entityIdCreator;

    public Long save(Entity entity) {
        entity.setId(entityIdCreator.generateNewId());
        return testRepository.save(entity).getId();
    }

    public List<Entity> findEntiy() {
        return testRepository.findAll();
    }

    public Entity findById(Long id) {
        return testRepository.findById(id);
    }
}
  • 목적 : 비즈니스 로직을 처리하는 방법

Test code 작성

  • TestService를 비즈니스 로직 test
  • repository, creator id 는 mock 객체로 사용 예정

1. Spring 없이 Mockito를 이용한 Test 코드 작성

1.1 Mockito 사용해서 test code 작성하기

Test code 설명

class MockitoTest {

    TestService testService;
    TestRepository testRepository;
    EntityIdCreator entityIdCreator;

    @BeforeEach
    void setUp() { // mock 객체 생성
        testRepository = Mockito.mock(TestRepository.class);
        entityIdCreator = Mockito.mock(EntityIdCreator.class);

        testService = new TestService(testRepository, entityIdCreator);
    }

    @Test
    void createWithMockTest() throws Exception {
        // given
        Entity entity = Entity.builder()
                .id(1L)
                .name("name")
                .build();

        Mockito.doAnswer(invocation -> 1L)
                .when(entityIdCreator).generateNewId();
        Mockito.doAnswer(invocation -> entity)
                .when(testRepository).save(entity);

        // when
        Long saveId = testService.save(entity);

        // then
        assertEquals(1L, saveId);
        Mockito.verify(testRepository, Mockito.times(1)).save(entity);
        Mockito.verify(entityIdCreator, Mockito.times(1)).generateNewId();
    }
}
  • setUp
    • mock 객체를 직접 생성해서 주입을 해주는 역할
    • Mockito.mock 을 이용해 mock 객체 생성
  • mock객체가 어떤 메서드를 호출했을때 기대 되는 반환 값을 설정 할 수 있습니다.
Mockito.doAnswer(invocation -> 1L)
                .when(entityIdCreator).generateNewId();
  • doAnswer를 통해 반환 값을 설정
  • when 을 통해 어떤 객체인지 설정
  • 함수 이름을 사용해서 함수를 호출 하는것 처럼 설정합니다.
  • Mockito.verify일 경우 함수가 몇 번 호출 되었는지 나타냅니다.
    • Mockito.verify(testRepository, Mockito.times(1)).save(entity);를 해석하면 testRepository에 있는 save라는 함수는 Mockito.times(1)를 통해 1번만 호출되어있다는 것을 확인할 수 있다는 의미 입니다.

Stub이 생성되지 않는 경우

  • testRepository는 save, findAll, findById 총 3가지 method 로 구성, 하지만 save만 mock stub으로 설정했습니다. 그러므로 finAll, findById는 Stub으로 설정이 되어있지 않아 test code에서 호출하면 타입별로 default 값이 반환 됩니다.
  • default 값 : Answers.RETURNS_DEFAULTS 에 설정이 되어있습니다.

1.2 @Mock 방식

  • Mockito.mock()으로 객체 생성한 것과 동일한 결과를 반환 합니다.
  • @Mock@ExtendWith(MockitoExtension.class)을 사용하고 싶을 경우 MockitoExtension class를 import 해와야 합니다.
  • 아래 예제에서는 class위에 @ExtendWith(MockitoExtension.class) 설정

Test code 작성

@ExtendWith(MockitoExtension.class)
class MockAnnotationTest {
    @InjectMocks
    TestService testService;
    @Mock
    TestRepository testRepository;
    @Mock
    EntityIdCreator entityIdCreator;

    @Test
    void mockInjectionTest() throws Exception {
        // given
        Entity entity = Entity.builder()
                .id(1L)
                .name("name")
                .build();

        Mockito.doAnswer(invocation -> 1L)
                .when(entityIdCreator).generateNewId();
        Mockito.doAnswer(invocation -> entity)
                .when(testRepository).save(entity);

        // when
        Long saveId = testService.save(entity);

        // then
        assertEquals(1L, saveId);
        Mockito.verify(testRepository, Mockito.times(1)).save(entity);
        Mockito.verify(entityIdCreator, Mockito.times(1)).generateNewId();
    }
}

@InjectMocks

  • @Mock으로 생성한 객체를 MockInjection을 통해 주입을 해줍니다.

1.3 @Spy

Spy 정의

  • Spy일 경우 doAnswer을 통해 stub을 설정하지 않을 경우 원래 기본 비즈니스 로직을 실행합니다.

Test code 작성

@ExtendWith(MockitoExtension.class)
class SpyTest {
    @InjectMocks
    TestService testService;
    @Spy
    TestRepository testRepository;
    @Spy
    EntityIdCreator entityIdCreator;

    @Test
    void mockInjectionTest() throws Exception {
        // given
        Entity entity = Entity.builder()
                .id(1L)
                .name("name")
                .build();

        Mockito.doAnswer(invocation -> 1L)
                .when(entityIdCreator).generateNewId();
        Mockito.doAnswer(invocation -> Collections.emptyList())
                .when(testRepository).findAll();

        // when
        Long saveId = testService.save(entity);
        Entity findEntity = testService.findById(1L);
        List<Entity> list = testService.findEntiy();


        // then
        assertEquals(0, list.size());
        assertEquals(1L, saveId);
        assertEquals(1L, findEntity.getId());
        Mockito.verify(testRepository, Mockito.times(1)).findAll();
        Mockito.verify(entityIdCreator, Mockito.times(1)).generateNewId();
    }
}
  • 현재 code에서도 @IndectMocks를 통해 @Spy로 생성된 객체를 주입 받고 있습니다.
  • 현재 예시에서는 findByIdfindEntiy는 stub을 설정하지 않았습니다. 그렇기 때문에 원래testRepository의 함수를 호출 하게 됩니다.

2. SpringBoot를 이용해서 Mockito 사용하기

종류

@MockBean
@SpyBean

2.1 @MockBean

Test code 작성

@SpringBootTest
public class MockBeanTest {
    @Autowired
    TestService testService;
    @MockBean
    TestRepository testRepository;
    @MockBean
    EntityIdCreator entityIdCreator;

    @Test
    void mockInjectionTest() throws Exception {
        // given
        Entity entity = Entity.builder()
                .id(1L)
                .name("name")
                .build();

        willReturn(1L)
                .given(entityIdCreator).generateNewId();
        willReturn(entity)
                .given(testRepository).save(entity);

        // when
        Long saveId = testService.save(entity);

        // then
        assertEquals(1L, saveId);
        Mockito.verify(testRepository, Mockito.times(1)).save(entity);
        Mockito.verify(entityIdCreator, Mockito.times(1)).generateNewId();
    }
}
  • MockBean은 해당 annotation이 붙은 객체를 mock 객체로 생성하고 Bean으로 등록합니다.
  • MockBean은 Mockito라는 package에 있지 않고 Spring package에 존재합니다. 그렇기 때문에 Spring 영역 dependency라는 것을 알 수 있습니다.
  • @SpringBootTest를 통해 Mock Bean객체는 @Autowired로 주입 받을 수 있습니다.
  • willReturn 이라는 함수를 통해 반환 값을 받을 수있습니다
  • given을 통해 어떤 객체의 stub을 설정할지 지정하고, 뒤에 메서드를 호출했기 때문에 해당 method가 호출 되면 willReturn에 지정된 값이 반환된다는 뜻입니다.

2.2 @SpyBean

@SpringBootTest
public class SpyBeanTest {
    @Autowired
    TestService testService;
    @SpyBean
    TestRepository testRepository;
    @SpyBean
    EntityIdCreator entityIdCreator;

    @Test
    void spyBeanTest() throws Exception {
        // given
        Entity entity = Entity.builder()
                .id(1L)
                .name("name")
                .build();

        willReturn(1L)
                .given(entityIdCreator).generateNewId();
        willReturn(Collections.emptyList())
                .given(testRepository).findAll();

        // when
        Long saveId = testService.save(entity);
        Entity findEntity = testService.findById(1L);
        List<Entity> list = testService.findEntiy();


        // then
        assertEquals(0, list.size());
        assertEquals(1L, saveId);
        assertEquals(1L, findEntity.getId());
        Mockito.verify(testRepository, Mockito.times(1)).findAll();
        Mockito.verify(entityIdCreator, Mockito.times(1)).generateNewId();
    }
}
  • 위에서 언급한 Spy와 동일합니다. 하지만 Bean 객체로 등록이 되기 때문에 @Autowired를 붙인 객체에 자동으로 주입이 되는 형태를 가집니다.
  • 유의점 : @SpyBean을 interface에 붙일 경우 interface의 구현체를 spring context 에 등록이 되어있어야 합니다.
  • -> MockBean인 경우 전체가 stub을 적어도 Default 값을 넣어서 stub을 형성하지만, Spy는 stub이 없는 경우 자동으로 구현된 메서드를 호출 하는 형태이기 때문입니다
반응형

'Spring > 개념' 카테고리의 다른 글

Logging  (0) 2021.12.17
Servlet과 Spring Container  (1) 2021.12.04
Spring Boot로 MySQL replication 연동하기  (0) 2021.10.06
04 Exception  (0) 2021.05.19
03 템플릿  (0) 2021.05.08