본문 바로가기
BackEnd/Project

[PT Manager] Ch04. Junit, Mockito를 사용한 테스트 작성

by 개발 Blog 2024. 8. 28.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

이번 시간에는 지난 시간에 완성한 프로젝트에 대해 JUnit과 Mockito를 이용해 단위 테스트를 작성해 본다. 단위 테스트는 소프트웨어 개발에서 중요한 부분으로, 코드의 개별적인 기능이 올바르게 작동하는지 확인하는 데 사용된다. JUnit과 Mockito는 Java에서 가장 널리 사용되는 테스트 프레임워크로, 각각 테스트 케이스 작성과 Mock 객체를 활용한 테스트에 유용하다.

1. 단위 테스트와 JUnit

단위 테스트(Unit Test)는 테스트가 가능한 최소 단위로 나눠서 기능의 유효성을 검증하는 방법이다. 각 함수나 메서드에 대해 조건에 따라 다양한 테스트 케이스를 작성할 수 있으며, 이를 통해 코드가 예상대로 동작하는지 확인한다.

 

JUnit은 Java에서 단위 테스트를 수행하기 위한 대표적인 Testing Framework이다. JUnit을 사용하면 코드의 개별 메서드에 대한 테스트를 쉽게 작성하고 실행할 수 있다. JUnit 4와 JUnit 5가 주로 사용되며, 두 버전 간의 차이점도 이해하는 것이 중요하다.

2. JUnit 4와 JUnit 5의 차이점

 

  • JUnit 4: Java 5 이상에서 사용 가능하며, 단일 runner만 지원한다. 테스트 작성이 간단하고 빠르게 할 수 있는 반면, 확장성이 제한적이다.
  • JUnit 5: Java 8 이상에서 사용 가능하며, JUnit Platform, JUnit Jupiter, JUnit Vintage로 구성된다. 한 번에 둘 이상의 runner를 확장할 수 있으며, 테스트 설명과 구성을 위한 새로운 기능들이 포함되어 있다.

3. Mock과 Mockito

Mockito는 테스트 작성 시 Mock 객체를 쉽게 생성하고 관리할 수 있게 해주는 라이브러리다. 실제 객체의 동작을 모방하여 테스트를 진행하며, 테스트 대상이 외부 서비스에 의존적일 때 유용하다.

  • Mock 객체: 실제 객체를 대체하는 가짜 객체로, 특정 메서드의 반환값을 지정하거나 호출 여부를 검증할 수 있다. 이를 통해 테스트 환경을 독립적으로 유지할 수 있다.

Mockito를 사용하면 테스트가 독립적으로 수행되며, 데이터베이스나 외부 API에 의존하지 않고도 로직을 검증할 수 있다.

4. 테스트 코드 예제

통계 서비스 테스트 (StatisticsServiceTest)

통계 데이터를 조회하고 이를 바탕으로 차트 데이터를 생성하는 StatisticsService의 로직을 검증하는 테스트를 작성했다. Mockito를 사용해 StatisticsRepository를 Mock 객체로 주입받아, 실제 데이터베이스 호출 없이도 테스트를 진행할 수 있도록 했다.

package com.example.pass.service.statistics;

import com.example.pass.repository.statistics.StatisticsRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
public class StatisticsServiceTest {

    @Mock // Mock 객체로 사용합니다.
    private StatisticsRepository statisticsRepository;
    @InjectMocks // @InjectMocks 클래스의 인스턴스를 생성하고 @Mock으로 생성된 객체를 주입합니다.
    private StatisticsService statisticsService;

    @Nested
    @DisplayName("통계 데이터를 기반으로 차트 만들기")
    class MakeChartData {
        final LocalDateTime to = LocalDateTime.of(2024, 8, 10, 0, 0);

        @DisplayName("통계 데이터가 있을 때")
        @Test
        void makeChartData_when_hasStatistics() {
            // given
            List<AggregatedStatistics> statisticsList = List.of(
                    new AggregatedStatistics(to.minusDays(1), 15, 10, 5),
                    new AggregatedStatistics(to, 10, 8, 2)
            );

            // when
            when(statisticsRepository.findByStatisticsAtBetweenAndGroupBy(eq(to.minusDays(10)), eq(to))).thenReturn(statisticsList);

            final ChartData chartData = statisticsService.makeChartData(to);

            // then
            verify(statisticsRepository, times(1)).findByStatisticsAtBetweenAndGroupBy(eq(to.minusDays(10)), eq(to));

            assertNotNull(chartData);
            assertEquals(new ArrayList<>(List.of("08-09", "08-10")), chartData.getLabels());
            assertEquals(new ArrayList<>(List.of(10L, 8L)), chartData.getAttendedCounts());
            assertEquals(new ArrayList<>(List.of(5L, 2L)), chartData.getCancelledCounts());

        }

        @DisplayName("통계 데이터가 없을 때")
        @Test
        void makeChartData_when_notHasStatistics() {
            // when
            when(statisticsRepository.findByStatisticsAtBetweenAndGroupBy(eq(to.minusDays(10)), eq(to))).thenReturn(Collections.emptyList());

            final ChartData chartData = statisticsService.makeChartData(to);

            // then
            verify(statisticsRepository, times(1)).findByStatisticsAtBetweenAndGroupBy(eq(to.minusDays(10)), eq(to));

            assertNotNull(chartData);
            assertTrue(chartData.getLabels().isEmpty());
            assertTrue(chartData.getAttendedCounts().isEmpty());
            assertTrue(chartData.getCancelledCounts().isEmpty());

        }

    }

    @DisplayName("차트 데이터 만들기")
    @Test
    public void test_makeChartData() {
        // given  to = 2024-08-10 00:00
        final LocalDateTime to = LocalDateTime.of(2024, 8, 10, 0, 0);

        List<AggregatedStatistics> statisticsList = List.of(
                new AggregatedStatistics(to.minusDays(1), 15, 10, 5),
                new AggregatedStatistics(to, 10, 8, 2)
        );

        // when
        // statisticsRepository Mock 객체가 findByStatisticsAtBetweenAndGroupBy()를 실행할 때 statisticsList를 반환합니다.
        when(statisticsRepository.findByStatisticsAtBetweenAndGroupBy(eq(to.minusDays(10)), eq(to))).thenReturn(statisticsList);

        final ChartData chartData = statisticsService.makeChartData(to);

        // then
        // findByStatisticsAtBetweenAndGroupBy()가 1번 호출되었는지 검증합니다.
        verify(statisticsRepository, times(1)).findByStatisticsAtBetweenAndGroupBy(eq(to.minusDays(10)), eq(to));

        assertNotNull(chartData);
        assertEquals(new ArrayList<>(List.of("08-09", "08-10")), chartData.getLabels());
        assertEquals(new ArrayList<>(List.of(10L, 8L)), chartData.getAttendedCounts());
        assertEquals(new ArrayList<>(List.of(5L, 2L)), chartData.getCancelledCounts());

    }

}

@DisplayName("통계 데이터를 기반으로 차트 만들기")

이 @DisplayName은 MakeChartData라는 내부 클래스에 적용되어 있으며, 이 클래스는 통계 데이터를 기반으로 차트를 생성하는 로직을 테스트하는 그룹을 정의한다. 이 그룹 내에서 두 개의 세부 테스트 케이스가 존재한다.

 

@DisplayName("통계 데이터가 있을 때")

이 테스트는 통계 데이터가 있을 때라는 시나리오를 다룬다. 주어진 날짜 범위에 해당하는 통계 데이터가 존재할 때, 이 데이터를 바탕으로 ChartData 객체를 생성하는 로직이 제대로 작동하는지를 검증한다.

  • 설정(Given): 가짜 통계 데이터 리스트(AggregatedStatistics)를 미리 준비한다.
  • 동작(When): Mock된 statisticsRepository 객체가 findByStatisticsAtBetweenAndGroupBy 메서드를 호출할 때 준비된 통계 데이터를 반환하도록 설정한다. 그런 다음, statisticsService.makeChartData(to)를 호출하여 차트 데이터를 생성한다.
  • 검증(Then):
    • statisticsRepository의 메서드가 한 번 호출되었는지 확인한다.
    • 반환된 ChartData 객체가 null이 아닌지 확인하고, 데이터가 올바르게 매핑되었는지 assertEquals로 검증한다.

@DisplayName("통계 데이터가 없을 때")

이 테스트는 통계 데이터가 없을 때라는 시나리오를 다룬다. 주어진 날짜 범위에 해당하는 통계 데이터가 존재하지 않을 때, 빈 ChartData 객체를 올바르게 생성하는지를 검증한다.

  • 설정(Given): 통계 데이터를 반환하지 않도록 설정한다.
  • 동작(When): Mock된 statisticsRepository 객체가 findByStatisticsAtBetweenAndGroupBy 메서드를 호출할 때 빈 리스트를 반환하도록 설정한다. 그런 다음, statisticsService.makeChartData(to)를 호출하여 차트 데이터를 생성한다.
  • 검증(Then):
    • statisticsRepository의 메서드가 한 번 호출되었는지 확인한다.
    • 반환된 ChartData 객체가 null이 아닌지 확인하고, labels, attendedCounts, cancelledCounts 리스트가 모두 비어 있는지 assertTrue로 검증한다.

@DisplayName("차트 데이터 만들기")

이 테스트는 통계 데이터를 기반으로 차트 만들기라는 그룹에 속하지 않고 별도로 존재하는 테스트이다. 이 테스트는 전체적으로 makeChartData 메서드가 주어진 통계 데이터를 바탕으로 ChartData 객체를 올바르게 생성하는지 확인한다.

  • 설정(Given): 가짜 통계 데이터 리스트(AggregatedStatistics)를 미리 준비한다.
  • 동작(When): Mock된 statisticsRepository 객체가 findByStatisticsAtBetweenAndGroupBy 메서드를 호출할 때 준비된 통계 데이터를 반환하도록 설정한다. 그런 다음, statisticsService.makeChartData(to)를 호출하여 차트 데이터를 생성한다.
  • 검증(Then):
    • statisticsRepository의 메서드가 한 번 호출되었는지 확인한다.
    • 반환된 ChartData 객체가 null이 아닌지 확인하고, 데이터가 올바르게 매핑되었는지 assertEquals로 검증한다.

 

패스 모델 매퍼 테스트 (PassModelMapperTest)

PassModelMapper의 매핑 로직이 올바르게 작동하는지 확인하기 위해 작성한 단위 테스트이다. 주어진 패스 엔티티 리스트를 매핑하여 도메인 객체로 변환하는 로직을 테스트한다.

package com.example.pass.service.pass;

import com.example.pass.repository.packaze.PackageEntity;
import com.example.pass.repository.pass.PassEntity;
import com.example.pass.repository.pass.PassStatus;
import org.junit.jupiter.api.Test;

import java.time.LocalDateTime;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class PassModelMapperTest {
    @Test
    public void test_toPasses() {
        // when
        final LocalDateTime now = LocalDateTime.now();

        final PackageEntity packageEntity = new PackageEntity();
        packageEntity.setPackageSeq(1);
        packageEntity.setPackageName("패키지1");
        packageEntity.setCount(10);
        packageEntity.setPeriod(30);

        final PassEntity passEntity = new PassEntity();
        passEntity.setPassSeq(1);
        passEntity.setStatus(PassStatus.READY);
        passEntity.setRemainingCount(10);
        passEntity.setStartedAt(now.plusDays(1));
        passEntity.setEndedAt(passEntity.getStartedAt().plusDays(30));
        passEntity.setPackageSeq(1);
        passEntity.setPackageEntity(packageEntity);

        // given
        final List<Pass> passes = PassModelMapper.INSTANCE.map(List.of(passEntity));

        // then
        assertEquals(1, passes.size());
        final Pass pass = passes.get(0);

    }

}
  • 테스트 메서드: test_toPasses
    • 이 메서드는 PassModelMapper의 map 메서드를 테스트하기 위해 작성된 단위 테스트이다.
  • 테스트 데이터 생성 (when 섹션):
    • now: 현재 시간을 기준으로 LocalDateTime 객체를 생성한다.
    • PackageEntity: 테스트에 사용할 패키지 엔티티를 생성한다. 이 엔티티는 다음과 같은 속성을 가진다:
      • packageSeq: 1
      • packageName: "패키지1"
      • count: 10
      • period: 30 (일)
    • PassEntity: 패스 엔티티를 생성하고, 위에서 생성한 PackageEntity와 연결한다. 이 엔티티는 다음과 같은 속성을 가진다:
      • passSeq: 1
      • status: PassStatus.READY
      • remainingCount: 10
      • startedAt: 현재 시간 기준으로 1일 뒤
      • endedAt: startedAt으로부터 30일 뒤
      • packageSeq: 1 (위에서 생성한 PackageEntity와 매핑)
  • 매핑 실행 (given 섹션):
    • PassModelMapper.INSTANCE.map: 이 메서드를 호출하여 PassEntity 리스트를 Pass 리스트로 변환한다.
    • List.of(passEntity): 변환할 PassEntity 객체를 리스트로 감싼 후 map 메서드에 전달한다.
  • 검증 (then 섹션):
    • assertEquals(1, passes.size()): 변환된 Pass 리스트의 크기가 1인지 확인한다. 이는 PassEntity 리스트에 하나의 엔티티만 있었기 때문에 기대되는 결과이다.
    • final Pass pass = passes.get(0): 변환된 Pass 객체를 가져온다. 이 객체의 속성들이 올바르게 매핑되었는지 추가적인 검증이 가능하다.

 

이번 시간에는 StatisticsServiceTest와 PassModelMapperTest 두 개의 테스트를 통해 서비스 로직과 매핑 로직의 정확성을 검증했다. 이를 통해 애플리케이션의 핵심 기능이 올바르게 동작함을 확인할 수 있었다. 앞으로도 다양한 테스트 케이스를 추가하여 코드의 안정성과 신뢰성을 지속적으로 강화할 예정이다.