Post

의존성 주입과 테스트(2) - Mockito

테스트 코드: 실제 데이터베이스 연결 없이 테스트하기

이전 포스트에서는 생성자 주입 방식으로 의존성 주입을 변경했습니다. 이번에는 Mockito를 사용하여 테스트 코드를 작성해보았습니다.

@Mock으로 변경

실제 데이터베이스와 연결하여 테스트를 진행할 수도 있었지만, 데이터베이스에 많은 데이터가 쌓여 있다 보니 테스트 속도가 느려질 수 있을 것 같다는 생각이 들었습니다.

테스트 속도를 향상시키고, 서비스 의존성 방식을 변경하여 외부 의존성을 제거하고 특정 컴포넌트의 동작만을 테스트하고 싶었습니다. 또한, 예외 상황이나 특정 데이터 등을 쉽게 테스트하고 싶어서 Mock으로 변경하기로 결정했습니다.

@Autowired vs @Mock

테스트 코드에서 @Autowired@Mock을 사용하는 방법이 있습니다.

@Autowired
  • 특징: 실제 Spring 컨텍스트를 로드하여, 실제 빈(bean)들을 주입받아 테스트합니다.

  • 장점:
    • 실제 환경과 유사한 테스트가 가능하여, 통합 테스트에 유리합니다.

    • 전체 애플리케이션의 흐름을 점검할 수 있습니다.

  • 단점:
    • Spring 컨텍스트 로딩 시간이 필요하기 때문에 테스트가 느릴 수 있습니다.

    • 특정 컴포넌트의 독립적인 테스트가 어려울 수 있습니다. 여러 컴포넌트들이 연계되어 작동하므로 문제의 원인을 파악하기 힘들 수 있습니다.

@Mock
  • 특징: Mockito 등을 사용하여 모의 객체를 생성하고 주입합니다.

  • 장점:
    • 모의 객체를 사용하므로 Spring 컨텍스트를 로드할 필요가 없기 때문에 빠른 테스트가 가능합니다.

    • 특정 컴포넌트를 독립적으로 테스트할 수 있습니다. 외부 의존성을 제거하고, 테스트하려는 컴포넌트에 집중할 수 있습니다.

    • 모의 객체의 동작을 원하는 대로 설정할 수 있어 다양한 시나리오를 쉽게 테스트할 수 있습니다.

  • 단점:
    • 실제 환경과의 차이가 있을 수 있습니다. 모의 객체는 실제 객체와 완전히 동일하게 동작하지 않을 수 있습니다.

    • 통합 테스트를 수행하기에는 적절하지 않을 수 있습니다. 모의 객체는 실제 데이터베이스나 네트워크와 상호작용하지 않기 때문입니다.

문제점

Mock으로 테스트를 진행하는 과정에서 데이터를 저장하는 save 메서드를 호출했음에도 불구하고, findAll 메서드를 통해 데이터를 조회할 때 항상 0개의 데이터만 반환되는 문제가 발생했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@ExtendWith(MockitoExtension.class)
public class NovelCrawlerServiceTest {

    @Mock
    private NovelRepository novelRepository;

    @InjectMocks
    private NovelCrawlerService novelCrawlerService;

    @DisplayName("데이터베이스에 소설 저장 및 조회 테스트")
    @Test
    void saveNovelsTest() {
        ContentDTO content = new ContentDTO("Title", "coverImg", "summary", "genre", false, "테스트ID");

        List<ContentDTO> list = new ArrayList<>();
        list.add(content);

        novelCrawlerService.saveNovels(list, "네이버시리즈");
        Novel result = novelCrawlerService.getDataByTitle("Title");

        // 적절한 Mockito 검증 로직 추가
        assertEquals("Title", result.getTitle());
        assertEquals("coverImg", result.getCoverImg());
        assertEquals("summary", result.getSummary());
        assertEquals("genre", result.getGenre());
        assertEquals(1, result.getSites().size());
        assertEquals("네이버시리즈", result.getSites().get(0).getSite().getName());
        assertEquals("테스트ID", result.getSites().get(0).getProductId());
        deleteNovelTestDataByTitle("Title");
    }

위 코드에서 result가 null로 뜨는 문제가 발생하여, 데이터가 DB에 저장되지 않는 문제를 겪었습니다.

문제 해결 : 최종 코드

Mock 객체를 사용하면서 실제 데이터베이스와의 연동이 이루어지지 않기 때문에 Mock 객체를 사용할 때는 해당 클래스의 동작을 명확히 설정해줘야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@ExtendWith(MockitoExtension.class)
public class NovelCrawlerServiceTest {

    @Mock
    private NovelRepository novelRepository;

    @InjectMocks
    private NovelCrawlerService novelCrawlerService;

    private ContentDTO content;
    private Novel mockNovel;

    @BeforeEach
    void setUp() {
        // 공통 데이터 설정
        content = new ContentDTO("Title", "coverImg", "summary", "genre", false, "테스트ID");
        mockNovel = new Novel(content.getTitle(), content.getCoverImg(), content.getSummary(),
                content.getGenre(), content.isAdultContent());
    }

    @DisplayName("소설 저장 테스트")
    @Test
    public void saveNovelsTest() {
        List<ContentDTO> list = new ArrayList<>();
        list.add(content);

        // Given: 초기 상태에서는 소설이 존재하지 않음
        when(novelRepository.findByTitle("Title")).thenReturn(null);

        // When: 소설을 저장
        novelCrawlerService.saveNovels(list, "네이버시리즈");

        // Then: 소설이 저장되었는지 확인
        verify(novelRepository, times(1)).save(any(Novel.class));
    }
    //테스트 코드..
}

  • 초기 상태 설정 : @BeforeEach 메서드를 추가하여 각 테스트 실행 전에 공통으로 사용할 데이터를 설정했습니다. 이를 통해 각 테스트가 독립적으로 동일한 초기 상태에서 시작될 수 있도록 했습니다.

  • Mock 설정 추가 : when(novelRepository.findByTitle("Title")).thenReturn(null);을 통해 초기 상태에서 소설이 존재하지 않는다는 것을 명시했습니다. 이를 통해 saveNovels 메서드를 호출할 때 실제로 새로운 소설을 저장하도록 유도했습니다.

  • 검증 로직 추가 : verify(novelRepository, times(1)).save(any(Novel.class));을 통해 novelRepositorysave 메서드가 정확히 한 번 호출되었는지 검증했습니다. 이를 통해 소설이 제대로 저장되었는지 확인할 수 있었습니다.

위와 같이 수정된 코드를 통해 Mock 객체를 활용한 테스트에서 발생했던 문제를 해결하고, 원하는 테스트 시나리오를 성공적으로 검증할 수 있었습니다.

요약

Mockito를 사용하여 데이터베이스와의 상호작용을 모의(mock)하기 때문에, 데이터베이스에 저장되는 것처럼 시뮬레이션할 뿐입니다. 이를 통해 특정 동작을 검증할 수 있습니다.

Mockito를 사용한 테스트는 실제 데이터베이스와 연결되지 않고, 데이터베이스와의 상호작용을 모의 객체로 대체하여 수행합니다. verify 메서드를 사용하여 save 메서드가 호출되었는지 확인할 수 있지만, 실제로 데이터베이스에 데이터가 저장되지는 않습니다.

Mock 객체를 활용한 테스트에서는 테스트 속도를 빠르게 유지하고, 특정 컴포넌트의 동작을 독립적으로 검증할 수 있습니다. 이는 다양한 시나리오를 쉽게 테스트하고, 외부 의존성에 영향을 받지 않는 안정적인 테스트 환경을 제공하는 데 유용합니다.

This post is licensed under CC BY 4.0 by the author.