About Project/My Projects

Vivim : 고객사와 개발사 간의 프로젝트 공유 웹 서비스 만들기 (Spring Boot, React, MySQL, Team Project)

마이트너 2025. 4. 1. 18:29

테스트

 

  • Mockito + JUnit 방식 (위 예제와 같은 방식)
    • 장점:
      • 개별 의존성을 모의(mock)하여 단위 테스트에 집중할 수 있음
      • 테스트가 빠르고, 간단한 단위 테스트 작성에 적합함
    • 단점:
      • 많은 모의 객체와 반복되는 설정 코드가 필요할 수 있음
      • 복잡한 객체 생성 로직이 중복되면 셋업 코드가 장황해질 수 있음
  • BDD 스타일 (Given-When-Then)로 작성
    • 장점:
      • 테스트의 흐름이 자연어에 가깝게 표현되어 가독성이 높음
      • Mockito의 BDDMockito를 사용하면 테스트의 의도를 명확하게 전달할 수 있음
    • 단점:
      • 팀 내에 BDD 패턴에 익숙하지 않다면 초기 학습 비용이 있음
      • 기존 코드와의 일관성 문제가 발생할 수 있음
  • Spring Boot Test 활용 (예: @SpringBootTest, @DataJpaTest)
    • 장점:
      • 실제 스프링 컨텍스트를 로드하여 통합 테스트와 유사하게 테스트할 수 있음
      • 레포지토리나 서비스 간의 실제 연동을 검증할 수 있음
    • 단점:
      • 테스트 실행 속도가 느려질 수 있음
      • 단위 테스트보다는 통합 테스트에 가깝게 동작하므로, 범위가 넓어질 수 있음
  • Spock 또는 AssertJ 같은 대체 테스트 프레임워크 사용
    • 장점:
      • Spock은 Groovy 기반으로 보다 직관적이고 간결한 테스트 코드를 작성할 수 있음
      • AssertJ를 사용하면 가독성이 높은 어설션 구문을 사용할 수 있음
    • 단점:
      • 기존 Java 기반 프로젝트에 도입 시 추가 학습이 필요할 수 있음
      • 환경 설정과 빌드 설정이 추가될 수 있음

 

 

package com.welcommu.moduleservice.projectProgess;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.welcommu.moduledomain.project.Project;
import com.welcommu.moduledomain.projectprogress.ProjectProgress;
import com.welcommu.modulerepository.project.ProjectRepository;
import com.welcommu.modulerepository.projectprogress.ProjectProgressRepository;
import com.welcommu.moduleservice.projectProgess.dto.ProgressCreateRequest;
import java.time.LocalDateTime;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
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;

@ExtendWith(MockitoExtension.class)
class ProjectProgressServiceTest {

	// Mockicto 라이브러리를 이용한 Mock 객체 생성
    @Mock
    private ProjectRepository projectRepository;

    @Mock
    private ProjectProgressRepository progressRepository;

    @InjectMocks
    private ProjectProgressService projectProgressService;

    // 도메인 엔티티는 그 자체로 단순한 데이터 객체이며, 
    // 내부에 복잡한 로직이나 외부 의존성이 없기 때문에 굳이 목(mock) 객체로 생성할 필요가 없음
    private Project project;
    private ProjectProgress projectProgress;

    @BeforeEach
    public void setUp() {
    
        // 테스트용 프로젝트 생성
        project = new Project();
        project.setId(1L);
        project.setName("Test Project");
        project.setCreatedAt(LocalDateTime.now());

        // 테스트용 프로젝트 진행 상태 생성
        projectProgress = new ProjectProgress();
        projectProgress.setId(1L);
        projectProgress.setName("Initial Progress");
        projectProgress.setProject(project);
    }


    @Test
    public void testCreateProgress() {

        Long projectId = 1L;
        log.info("\n테스트 : CreateProgress with projectId: {}", projectId);

        // ProgressCreateRequest 를 모의(Mock) 객체로 생성 (by Mockito Library)
        ProgressCreateRequest request = mock(ProgressCreateRequest.class);
        ProjectProgress progressRequest = new ProjectProgress();
        progressRequest.setName("생성된 단계");

        // toEntity 메서드에 대해 stub 처리
        when(request.toEntity(project)).thenReturn(progressRequest);
        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
        when(progressRepository.findMaxPositionByProjectId(projectId)).thenReturn(Optional.of(6.0f));

        projectProgressService.createProgress(projectId, request);
        log.info("\n테스트 : After createProgress: progress position is {}", progressRequest.getPosition());

        // 생성된 progress 의 position 값이 최대값(10.0f)으로 설정되었는지 확인
        assertEquals("생성된 단계", progressRequest.getName());
        assertEquals(6.0f, progressRequest.getPosition());
        verify(progressRepository).save(progressRequest);

        log.info("\n테스트 : Completed testCreateProgress successfully.");
    }
    
    @Test
    public void testUpdateProgress_Success() {
    
        Long projectId = 1L;
        Long progressId = 1L;

        ProgressUpdateRequest request = new ProgressUpdateRequest();

        // 초기값으로 다른 이름을 설정해서 변경되는 것을 확인할 수 있도록 합니다.
        request.setName("매칭되는 상황에서 수정된 단계");
        request.setPosition(4.5f);

        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
        when(progressRepository.findById(progressId)).thenReturn(Optional.of(projectProgress));

        log.info("Before update: projectProgress name = {}", projectProgress.getName());
        log.info("Before update: projectProgress position = {}", projectProgress.getPosition());
        
        projectProgressService.updateProgress(projectId, progressId, request);
        
        log.info("After update: projectProgress name = {}", projectProgress.getName());
        log.info("After update: projectProgress position = {}", projectProgress.getPosition());

        assertEquals("매칭되는 상황에서 수정된 단계", projectProgress.getName());
        verify(progressRepository).save(projectProgress);
    }

    @Test
    public void testUpdateProgress_Mismatch() {
        Long projectId = 1L;
        Long progressId = 1L;
        ProgressCreateRequest request = new ProgressCreateRequest();
        projectProgress.setName(request.getName());

        Project differentProject = new Project();
        differentProject.setId(2L);
        differentProject.setName("매칭되지 않는 경우에 수정된 단계");
        differentProject.setCreatedAt(project.getCreatedAt());
        projectProgress.setProject(differentProject);

        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
        when(progressRepository.findById(progressId)).thenReturn(Optional.of(projectProgress));

        CustomException exception = assertThrows(CustomException.class, () -> {
            projectProgressService.updateProgress(projectId, progressId, request);
        });
        assertEquals(CustomErrorCode.MISMATCH_PROJECT_PROGRESS, exception.getErrorCode());
    }

    @Test
    public void testDeleteProgress_Success() {
        Long projectId = 1L;
        Long progressId = 1L;
        projectProgress.setProject(project);

        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
        when(progressRepository.findById(progressId)).thenReturn(Optional.of(projectProgress));

        projectProgressService.deleteProgress(projectId, progressId);

        verify(progressRepository).delete(projectProgress);
    }

    @Test
    public void testDeleteProgress_Mismatch() {
        Long projectId = 1L;
        Long progressId = 1L;

        // 불일치 상황을 위해 다른 프로젝트 정보 설정
        Project differentProject = new Project();
        differentProject.setId(2L);
        differentProject.setName("매칭되지 않는 경우에 삭제된 단계");
        differentProject.setCreatedAt(project.getCreatedAt());
        projectProgress.setProject(differentProject);

        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
        when(progressRepository.findById(progressId)).thenReturn(Optional.of(projectProgress));

        CustomException exception = assertThrows(CustomException.class, () -> {
            projectProgressService.deleteProgress(projectId, progressId);
        });
        assertEquals(CustomErrorCode.MISMATCH_PROJECT_PROGRESS, exception.getErrorCode());
    }

    @Test
    public void testGetProgressList() {
        Long projectId = 1L;
        List<ProjectProgress> progressList = Collections.singletonList(projectProgress);

        when(projectRepository.findById(projectId)).thenReturn(Optional.of(project));
        when(progressRepository.findByProject(project)).thenReturn(progressList);

        ProgressListResponse response = projectProgressService.getProgressList(projectId);
        assertNotNull(response);
    }
}

 

  • @SpringBootTest를 사용하지 않는 경우(JUnit 5의 @ExtendWith + Mockito) : Spring Boot 없이 JUnit 5에서 Mockito를 사용할 때는 @ExtendWith(MockitoExtension.class)를 사용하여 Mockito 확장을 활성화할 수 있습니다. 이 방법은 @RunWith(MockitoJUnitRunner.class)와 비슷하지만 JUnit 5에 맞게 설정됩니다. 
    • 그렇다면 확장을 사용하지 않으면 어떨까요?
      • 테스트 실행 전후에 추가적인 동작(예: 리소스 초기화, 외부 시스템과의 연결 등)을 제어할 방법이 제한됩니다. 하지만 @ExtendWith를 사용하면 beforeEach, afterEach 메서드를 통해 테스트 메서드 전후의 작업을 제어할 수 있습니다.
    • 확장을 사용하면 뭐가 좋을까요?
      • @ExtendWith를 사용하여 확장 기능을 추가하면, 테스트 메서드의 실행을 조건부로 제어할 수 있습니다. 예를 들어, 특정 조건에 따라 테스트를 실행하거나, 특정 환경에서만 테스트가 실행되도록 설정할 수 있습니다.
      • 외부 라이브러리나 프레임워크의 확장 기능을 손쉽게 통합할 수 있습니다. 예를 들어, JUnit 5에서 제공하는 확장 외에도, Mockito나 Spring Test와 같은 외부 라이브러리의 기능을 통합할 때 유용합니다.
728x90