728x90
단위 테스트는 하나의 가장 작은 기능을 테스트하는 것을 목표로 함.
Mockito 란?
Mockito는 Java의 강력한 테스트 라이브러리로, 의존성을 가진 클래스의 동작을 시뮬레이션할 수 있는 Mock 객체를 제공합니다. 이를 통해 실제 구현이 아닌 가짜(Mock) 객체로 테스트를 수행하여 의존성을 줄이고, 단위 테스트를 쉽게 작성할 수 있습니다.
Junit 과 같이 사용하는 이유?
여기서 Junit4를 함께 사용하는 이유는, Mockito를 사용하는 것은 가짜로 데이터를 만들어 넣어 테스트에 필요한 Mock 객체를 만들어 줄 뿐이기 때문에 Mockito에서 제공하는 함수들로는 많은 종류의 Unit Test를 할 수 없기 때문이다. Mockito로 Mock 객체를 만들고, 더불어서 Junit4를 사용하여 Unit Test를 수행한다면 실제 구현된 부분을 건드리지 않고, 넓은 범위에서 Unit Test 가 가능할 것으로 보인다.
기본 용어
1. InjectMocks
Mock 객체를 자동으로 주입하여 테스트할 클래스의 인스턴스를 생성합니다. 즉, @Mock 또는 @Spy로 생성된 가짜 객체를 자동으로 주입시켜주는 어노테이션
@InjectMocks
private SomeController someController;
2. Mock
실제 객체 대신 동작을 시뮬레이션하기 위해 사용되는 객체입니다.
@Mock //Mock 객체 생성
private SomeService someService;
3. Stubbing
특정 메서드 호출에 대한 반환값을 정의합니다.
when(someService.someMethod()).thenReturn("mocked result");
ex)
when(directoryRepository.findById(2L)).thenReturn(Optional.of(parentDirectory));
4. Verification
Mock 객체의 메서드가 호출되었는지, 몇 번 호출되었는지 검증합니다.
verify(someService, times(1)).someMethod();
ex)
verify(directoryRepository, times(1)).findById(2L);
Stubbing 메서드
- when(mock.method()).thenReturn(value)
특정 호출에 대해 반환값 정의. - when(mock.method()).thenThrow(exception)
호출 시 예외 발생.
Verification 메서드
- verify(mock).method()
Mock 메서드 호출 여부 검증. - verify(mock, times(n)).method()
Mock 메서드 호출 횟수 검증. - verify(mock, never()).method()
메서드가 호출되지 않았음을 검증. - verifyNoInteractions(mock)
Mock 객체가 호출되지 않았음을 검증.
기본 템플릿
@ExtendWith(MockitoExtension.class) // JUnit5에서 Mockito 사용
class SomeServiceTest {
@InjectMocks
private SomeService someService; // 테스트할 클래스
@Mock
private DependencyService dependencyService; // 의존성
@BeforeEach
void setUp() {
// 필요한 초기화 설정
}
@Test
@DisplayName("테스트 메서드 설명")
void testSomeFunctionality() {
// 1. Stubbing: Mock 동작 정의
when(dependencyService.someMethod()).thenReturn("mocked value");
// 2. 테스트 대상 메서드 호출
String result = someService.someFunction();
// 3. Assertions: 결과 검증
assertEquals("expected value", result);
// 4. Verification: Mock 호출 검증
verify(dependencyService, times(1)).someMethod();
}
}
작성 예시
1. build.gradle
// mockito
testImplementation 'org.mockito:mockito-core:4.11.0'
testImplementation 'org.mockito:mockito-junit-jupiter:4.11.0'
2. test code 작성
package com.linkmoa.source.domain.directory.service;
import com.linkmoa.source.auth.oauth2.principal.PrincipalDetails;
import com.linkmoa.source.domain.directory.dto.request.DirectoryCreateReques;
import com.linkmoa.source.domain.directory.dto.response.ApiDirectoryResponseSpec;
import com.linkmoa.source.domain.directory.entity.Directory;
import com.linkmoa.source.domain.directory.repository.DirectoryRepository;
import com.linkmoa.source.domain.member.constant.Role;
import com.linkmoa.source.domain.member.entity.Member;
import com.linkmoa.source.domain.member.service.MemberService;
import com.linkmoa.source.domain.page.repository.PageRepository;
import com.linkmoa.source.global.command.constant.CommandType;
import com.linkmoa.source.global.dto.request.BaseRequest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import java.lang.reflect.Field;
import java.util.Optional;
import static org.hibernate.validator.internal.util.Contracts.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@Slf4j
class DirectoryServiceCreateTest {
@InjectMocks //DI를 @Mock 이나 @Spy로 생성된 mock 객체를 자동으로 주입해줌.
private DirectoryService directoryService;
@Mock // 실제 객체와 동일한 모의 객체 Mock 객체를 만들어줌.
private DirectoryRepository directoryRepository;
@Mock
private PageRepository pageRepository;
@Mock
private MemberService memberService;
@Mock
private PrincipalDetails principalDetails;
@BeforeEach
// 현재 클래스의 각 @Test 메소드보다 먼저 실행되어야함.
void setUp() throws NoSuchFieldException, IllegalAccessException {
// Member 객체 생성
Member member = Member.builder()
.email("test@example.com")
.password("password")
.role(Role.ROLE_USER)
.nickname("TestUser")
.provider("google")
.providerId("google123")
.build();
// Member의 id 필드 값 강제 설정
Field idField = member.getClass().getDeclaredField("id");
idField.setAccessible(true);
idField.set(member, 1L);
// PrincipalDetails 생성
principalDetails = new PrincipalDetails(member);
// lenient를 사용하여 memberService의 동작 설정을 무시 가능하도록 설정
lenient().when(memberService.findMemberByEmail("test@example.com")).thenReturn(member);
}
@Test
@DisplayName("parent directory가 없는 경우 directory 생성 테스트 ")
public void createDirectory_WithoutParentDirectory_Test() {
// given
DirectoryCreateReques requestDto = new DirectoryCreateReques(
new BaseRequest(1L, CommandType.CREATE),
"Test Directory",
null,
"Test Description"
);
//when
// directoryRepository.save 메서드 호출시 동작을 Mock 으로 설정
// 테스트 중에 실제 데이터베이스를 사용하지 않고, 저장되는 Directory 객체의 동작을 제어
//when : 특정 메서드 호출될 떄 실행될 동작(Stub)을 지정
//invocation : save 메서드 호출에 대한 정보를 담음 - 호출된 메서드의 인자/반환값 등을 다룰 수 있음.
when(directoryRepository.save(any(Directory.class))).thenAnswer(invocation -> {
Directory savedDirectory = invocation.getArgument(0); //save 메서드 호출시 전달된 첫번째 인자
// 테스트 환경에서는 JPA의 자동 ID 생성(@GeneratedValue)이 동작하지 않으므로,
// 테스트 중 ID를 직접 설정하기 위해 리플렉션을 사용.
// Directory 클래스의 id 라는 이름의 필드를 가져옴.
Field idField = Directory.class.getDeclaredField("id");
idField.setAccessible(true); //필드에 대한 접근 권한 부여
idField.set(savedDirectory, 1L); // ID를 강제로 1L로 설정
return savedDirectory; // Mock의 반환값 : save 메서드로 저장된 엔터티 객체
});
ApiDirectoryResponseSpec<Long> response = directoryService.createDirectory(requestDto, principalDetails);
// then
assertEquals(HttpStatus.OK, response.getHttpStatusCode());
assertEquals("Directory 생성에 성공했습니다.", response.getSuccessMessage());
log.info("getData {}", String.valueOf(response.getData()));
assertNotNull(response.getData()); //null이 아님을 확인함.
log.info("without Parent : {}",directoryRepository.getById(1L).getDirectoryName());
// 검증
//directoryRepository.save 메서드가 정확히 한 번 호출되었는지 확인함.
verify(directoryRepository, times(1)).save(any(Directory.class));
}
@Test
@DisplayName("parent directory가 있는 경우 directory 생성 테스트 ")
void createDirectory_WithParentDirectory_Test() throws Exception {
DirectoryCreateReques requestDto = new DirectoryCreateReques(
new BaseRequest(1L, CommandType.CREATE),
"Test Sub Directory",
2L,
"Test SubDescription"
);
Directory parentDirectory = Directory.builder()
.directoryName("Parent Directory")
.directoryDescription("Parent Description")
.build();
Directory newDirectory = Directory.builder()
.directoryName(requestDto.directoryName())
.directoryDescription(requestDto.directoryDescription())
.build();
parentDirectory.addChildDirectory(newDirectory);
// Mock 동작 설정
when(directoryRepository.findById(2L)).thenReturn(Optional.of(parentDirectory));
when(directoryRepository.save(any(Directory.class))).thenAnswer(invocation -> {
Directory savedDirectory = invocation.getArgument(0);
// 리플렉션으로 Directory의 id 필드에 값 설정
Field idField = Directory.class.getDeclaredField("id");
idField.setAccessible(true);
idField.set(savedDirectory, 3L); // ID를 강제로 설정
return savedDirectory;
});
// when
ApiDirectoryResponseSpec<Long> response = directoryService.createDirectory(requestDto, principalDetails);
// then
assertEquals(HttpStatus.OK, response.getHttpStatusCode());
assertEquals("Directory 생성에 성공했습니다.", response.getSuccessMessage());
assertNotNull(response.getData()); // null이 아님을 확인
assertEquals(3L, response.getData()); // 반환된 ID가 예상대로 1L인지 확인
// 검증
verify(directoryRepository, times(1)).findById(2L);
verify(directoryRepository, times(1)).save(any(Directory.class));
}
}
package com.linkmoa.source.domain.directory.service;
import com.linkmoa.source.auth.oauth2.principal.PrincipalDetails;
import com.linkmoa.source.domain.directory.dto.request.DirectoryIdRequest;
import com.linkmoa.source.domain.directory.dto.response.ApiDirectoryResponseSpec;
import com.linkmoa.source.domain.directory.entity.Directory;
import com.linkmoa.source.domain.directory.error.DirectoryErrorCode;
import com.linkmoa.source.domain.directory.exception.DirectoryException;
import com.linkmoa.source.domain.directory.repository.DirectoryRepository;
import com.linkmoa.source.domain.member.constant.Role;
import com.linkmoa.source.domain.member.entity.Member;
import com.linkmoa.source.domain.member.service.MemberService;
import com.linkmoa.source.global.command.constant.CommandType;
import com.linkmoa.source.global.dto.request.BaseRequest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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 org.springframework.http.HttpStatus;
import java.lang.reflect.Field;
import java.util.Optional;
import static com.linkmoa.source.global.command.constant.CommandType.EDIT;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@Slf4j
class DirectoryServiceDeleteTest {
@InjectMocks
private DirectoryService directoryService;
@Mock
private DirectoryRepository directoryRepository;
@Mock
private MemberService memberService;
private PrincipalDetails principalDetails;
@BeforeEach
void setUp() throws NoSuchFieldException, IllegalAccessException {
// Member 객체 생성
Member member = Member.builder()
.email("test@example.com")
.password("password")
.role(Role.ROLE_USER)
.nickname("TestUser")
.provider("google")
.providerId("google123")
.build();
// Member의 id 필드 값 강제 설정
Field idField = member.getClass().getDeclaredField("id");
idField.setAccessible(true);
idField.set(member, 1L);
// PrincipalDetails 생성
principalDetails = new PrincipalDetails(member);
// lenient를 사용하여 memberService의 동작 설정을 무시 가능하도록 설정
lenient().when(memberService.findMemberByEmail("test@example.com")).thenReturn(member);
}
@Test
@DisplayName("존재하는 디렉토리 삭제 테스트")
void directoryDelete_Success() throws NoSuchFieldException, IllegalAccessException {
// Given
DirectoryIdRequest requestDto = new DirectoryIdRequest(
new BaseRequest(1L, CommandType.EDIT), 1L);
Directory directory = Directory.builder()
.directoryName("Test Directory")
.directoryDescription("Test Description")
.build();
// Reflection으로 ID 설정
Field idField = Directory.class.getDeclaredField("id");
idField.setAccessible(true);
idField.set(directory, 1L);
// Mock 설정
when(directoryRepository.findById(1L)).thenReturn(Optional.of(directory));
// Mock delete 동작 설정
doAnswer(invocation -> {
Directory deletedDirectory = invocation.getArgument(0);
// 리플렉션으로 Directory의 id 필드에 값 설정 (테스트 시 동작 확인용)
Field idFieldInner = Directory.class.getDeclaredField("id");
idFieldInner.setAccessible(true);
idFieldInner.set(deletedDirectory, 1L); // ID를 강제로 설정
log.info("Deleted Directory ID: {}", deletedDirectory.getId());
return null; // void 메서드이므로 null 반환
}).when(directoryRepository).delete(any(Directory.class));
// When
ApiDirectoryResponseSpec<Long> response = directoryService.deleteDirectory(requestDto, principalDetails);
// Then
assertEquals(HttpStatus.OK, response.getHttpStatusCode());
assertEquals("Directory 삭제에 성공했습니다.", response.getSuccessMessage());
assertEquals(1L, response.getData()); // 반환된 ID 확인
log.info(response.getSuccessMessage());
// Verify
verify(directoryRepository, times(1)).delete(any(Directory.class)); // delete 호출 검증
}
@Test
@DisplayName("존재하지 않는 디렉토리 삭제 시 예외 발생 테스트")
void directoryDelete_NotFound() {
// Given
DirectoryIdRequest requestDto = new DirectoryIdRequest(
new BaseRequest(1L, CommandType.EDIT), 99L); // 존재하지 않는 ID 설정
// Mock 설정
when(directoryRepository.findById(99L)).thenReturn(Optional.empty());
// When & Then
DirectoryException exception = assertThrows(
DirectoryException.class,
() -> directoryService.deleteDirectory(requestDto, principalDetails)
);
// 예외 검증
assertEquals(DirectoryErrorCode.DIRECTORY_NOT_FOUND, exception.getDirectoryErrorCode());
// Verify
verify(directoryRepository, times(1)).findById(99L); // findById가 한 번 호출되었는지 검증
verify(directoryRepository, never()).delete(any()); // delete가 호출되지 않았는지 검증
}
}
728x90
'백엔드 공부' 카테고리의 다른 글
[Java Spring] Exception 발생 시 로그 및 HTTP 응답 에러 메시지 안뜨는 문제 해결 (1) | 2025.01.22 |
---|---|
[Java Spring 설계] MSA + Spring Cloud Eureka 개념 (0) | 2025.01.17 |