https://tech.kakaopay.com/post/mock-test-code/
1. 요약
- 테스트 코드는 Mock을 스프링 빈으로 등록하여 사용한다.
- 각 의존하는 객체는 실제 사용하는 것이 아닌, return 값을 미리 정해준다.
2. 문제
- 테스트 코드 작성 시, 의존하는 객체에 의해서 문제가 발생한다.
- 아래 코드는 AuthService의 login 함수를 테스트하는 코드다.
- 문제는 login 함수를 테스트 하기 위해서 DB에 데이터를 저장해야 한다.
- 즉, AuthService는 UserRepository(DB 저장 객체)에 의존적이다.
- 이와 같은 의존 객체가 많아질 수록, 테스트가 어려워진다.
@SpringBootTest
class AuthServiceTest {
private final AuthService authService;
private final UserRepository userRepository;
@Autowired
public AuthServiceTest(AuthService authService, UserRepository userRepository) {
this.authService = authService;
this.userRepository = userRepository;
}
public void 사용자가저장되어있다면로그인가능(){
//given
Users user = Users.builder()
.email("email1")
.password("password1")
.username("username1")
.phoneNumber("phoneNumber1")
.role(Role.USER)
.birthday(LocalDate.of(2002,6,23))
.build();
//when
userRepository.save(user);
//then
UserDto findUserDto = authService.login(user.getEmail(), user.getPassword());
assertEquals(findUserDto.getEmail(), user.getEmail());
}
}
3. 해결 방안
- Mock 객체 사용
- 즉 테스트 하려는 객체가 있다면, 의존하는 또 다른 객체의 반환값을 임의로 정해두는 것
3-1. Mocking 사용 예시
- 목적
- AuthService의 login 함수가 정상 작동하는지 확인한다.
- 방법
- AuthService가 사용하는 AuthRepository을 Mocking한다.
- Mocking된 AuthRepository는 껍데기만 가진 객체가 된다.(즉, 내부의 각 함수들은 있지만, 함수 내용은 없는 상태)
- AuthService의 login 함수가 사용하는 AuthRepository의 findByEmail을 직접 정의한다.(when)
- login 함수 호출 시, 직접 정의한 findByEmail 함수가 사용된다.
- login 함수는 정상적인 결과를 반환한다.
@SpringBootTest
class AuthServiceTest {
@MokitoBean
private AuthRepository authRepository;
@Autowired
private AuthService authService;
@Test
public void 사용자가저장되어있다면로그인가능(){
//given
Users user = Users.builder()
.email("email1")
.password("password1")
.username("username1")
.phoneNumber("phoneNumber1")
.role(Role.USER)
.birthday(LocalDate.of(2002,6,23))
.build();
//when
when(authRepository.findByEmail(user.getEmail())).thenReturn(user);
//then
UserDto findUserDto = authService.login(user.getEmail(), user.getPassword());
assertEquals(findUserDto.getEmail(), user.getEmail());
}
@Test
public void 사용자가저장돼어있지않다면로그인불가능(){
//given
Users user = Users.builder()
.email("email1")
.password("password1")
.username("username1")
.phoneNumber("phoneNumber1")
.role(Role.USER)
.birthday(LocalDate.of(2002,6,23))
.build();
//when
when(authRepository.findByEmail(user.getEmail())).thenReturn(null);
when(passwordEncoder.matches(user.getPassword(), user.getPassword())).thenReturn(true);
//then
UserDto findUserDto = authService.login(user.getEmail(), user.getPassword());
assertNull(findUserDto);
}
@Test
public void 사용자가저장돼어있지않다면로그인불가능(){
//given
Users user = Users.builder()
.email("email1")
.password("password1")
.username("username1")
.phoneNumber("phoneNumber1")
.role(Role.USER)
.birthday(LocalDate.of(2002,6,23))
.build();
//when
when(authRepository.findByEmail(user.getEmail())).thenReturn(null);
when(passwordEncoder.matches(user.getPassword(), user.getPassword())).thenReturn(true);
//then
UserDto findUserDto = authService.login(user.getEmail(), user.getPassword());
assertNull(findUserDto);
}
}
4. Mocking 문제 해결
- 문제
- ApplicationContext가 초기화되는 문제
- Mocking을 통해 새로운 스프링 빈이 만들어지면서 ApplicationContext가 초기화됨
- 예시
- AuthRepositoryTest 진행
- AuthRepository를 사용하는 Mock 전용 스프링 빈이 생성됐으니 ApplicationContext 초기화
- AuthServiceTest 진행
- AuthService를 사용하는 Mock 전용 스프링 빈이 생성됐으니 ApplicationContext 초기화
- ⇒ 각 테스트 코드에서 새로운 스프링 빈이 생성되므로, 테스트 코드 실행 시간이 계속 증가
- (각 테스트 객체가 N개 있으면, N번 Applicatoin Context 초기화한다는 것을 유추 가능)
- AuthRepositoryTest 진행
- ApplicationContext가 초기화되는 문제
@SpringBootTest
class AuthRepositoryTest {
@MockitoBean
private final AuthRepository authRepository;
-----------------------------------------------------------------------
@SpringBootTest
class AuthServiceTest {
@MockitoBean
private final AuthRepository authRepository;
@MockitoBean
private final AuthService authService;
- 기반 설명
- ApplicationContext는 스프링 컨테이너
- TestContext는 테스트 용도로 사용되는 스프링 컨테이너
- TestContext는 ApplicationContext의 스프링 빈을 사용해서 테스트를 진행
- ApplicationContext는 스프링 빈의 내용 등이 변경되면 초기화 진행
- Mocking은 새로운 스프링 빈을 만드는 것
- TestContext에서 Mocking으로 새로운 스프링 빈을 만들었으니 스프링 빈에 변경사항이 생긴 것
- 스프링 빈에 변경사항이 생겼으니, ApplicationContext는 초기화
- ⇒ Mock을 사용하여 새로운 객체를 만드는 것은 새로운 스프링 빈을 만드는 것이니 ApplicationContext를 초기화 시킴
- (출처는 아래 참고)
- 문제 해결
- TestConfig
- 사용하는 실제 객체를 스프링 빈에 등록 시, mock으로 등록
- 주의) 실제 객체와 mock 객체가 겹치므로, @Primary를 써서 mock 객체의 우선순위를 높여야함
- TestConfig
@TestConfiguration
public class TestConfig {
@Bean
@Primary
public AuthRepository authRepository(){
return mock(AuthRepository.class);
}
@Bean
@Primary
public AuthService authService(){
return mock(AuthService.class);
}
}
- 사용
- 주의) @Import로 스프링 빈을 주입한 config을 가져와야함
- (테스트 코드는 컴포넌트 대상에서 벗어나기 때문에 필요)
- 주의) @Import로 스프링 빈을 주입한 config을 가져와야함
//SpringBootTest : 스프링 빈 주입받기
//Import : 테스트 코드에 있는 스프링 빈은 스캔 대상X(따라서, 테스트 코드에 있는 스프링 빈 사용하고 싶으면 별도로 Import)
@SpringBootTest
@Import(TestConfig.class)
class AuthServiceTest {
private final AuthRepository authRepository;
private final AuthService authService;
@Autowired
public AuthServiceTest(AuthRepository authRepository, AuthService authService) {
this.authRepository = authRepository;
this.authService = authService;
}
@Test
public void 객체가Mock이라면진실(){
assertTrue(mockingDetails(authRepository).isMock());
}
}
https://velog.io/@glencode/테스트-코드에서-MockBean-사용의-문제점과-해결법
4. V1 - Config을 사용한 Mocking 사용법
- TestRepositoryConfig
- Service Test 계층에서 사용할 Repository를 mock으로 설정한다.
- 각 Controller, Service, Repository마다 1번씩 ApplicationContext 초기화가 일어나므로, N번에서 3번으로 초기화 횟수를 줄임
@TestConfiguration
public class TestRepositoryConfig {
@Bean
@Primary
public AuthRepository authRepository(){
return mock(AuthRepository.class);
}
@Bean
@Primary
public UserRepository userRepository(){
return mock(UserRepository.class);
}
@Bean
@Primary
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return mock(BCryptPasswordEncoder.class);
}
}
- AuthServiceTest
- TestRepositoryConfig을 통해 mock을 주입받는다
- AuthService를 사용 시, mock으로 선언된 Repository의 사용 방안을 정의한다
- 그 후, AuthService의 각 함수를 검증한다.
- 주의
- mock을 사용한 이후, 초기화해야한다.
- 아래 코드를 보면 authRepository의 findByEmail의 함수 내용을 정의했다.
- 따라서, 다른 함수로 넘어가면 해당 설정이 그대로 유지되는 상황이 발생한다.
- mock을 사용한 이후, 초기화해야한다.
when(authRepository.findByEmail(user.getEmail())).thenReturn(user);
- 해결
- 각 함수를 사용한 이후에는 사용된 mock을 초기화한다.
@AfterEach
void tearDown() {
Mockito.reset(authRepository);
}
전체 코드
//SpringBootTest : 스프링 빈 주입받기
//Import : 테스트 코드에 있는 스프링 빈은 스캔 대상X(따라서, 테스트 코드에 있는 스프링 빈 사용하고 싶으면 별도로 Import)
@SpringBootTest
@Import(TestRepositoryConfig.class)
class AuthServiceTest {
private final AuthRepository authRepository;
private final AuthService authService;
private final BCryptPasswordEncoder passwordEncoder;
@Autowired
public AuthServiceTest(AuthRepository authRepository, AuthService authService, BCryptPasswordEncoder passwordEncoder) {
this.authRepository = authRepository;
this.authService = authService;
this.passwordEncoder = passwordEncoder;
}
@AfterEach
void tearDown() {
Mockito.reset(authRepository);
}
@Test
public void 사용자가저장되어있다면로그인가능(){
//given
Users user = Users.builder()
.email("email1")
.password("password1")
.username("username1")
.phoneNumber("phoneNumber1")
.role(Role.USER)
.birthday(LocalDate.of(2002,6,23))
.build();
//when
when(authRepository.findByEmail(user.getEmail())).thenReturn(user);
when(passwordEncoder.matches(user.getPassword(), user.getPassword())).thenReturn(true);
//then
UserDto findUserDto = authService.login(user.getEmail(), user.getPassword());
assertEquals(findUserDto.getEmail(), user.getEmail());
}
}
6. @WebMvcTest
- Controller 관련 어노테이션을 가진 스프링 빈만 가져옴
- (주로 Contoller 객체 테스트 시에 사용)
- 예시
@WebMvcTest(controllers = AuthController.class,
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = OncePerRequestFilter.class)
})
@ImportAutoConfiguration(TestSecurityConfig.class)
@Import(TestServiceConfig.class)
class AuthControllerTest {
private final AuthService authService;
private final JwtTokenProvider jwtTokenProvider;
private final MockMvc mvc;
private ObjectMapper mapper = new ObjectMapper();
@Autowired
public AuthControllerTest(AuthService authService, JwtTokenProvider jwtTokenProvider, MockMvc mvc) {
this.authService = authService;
this.jwtTokenProvider = jwtTokenProvider;
this.mvc = mvc;
}
@AfterEach
void tearDown() {
Mockito.reset(authService);
}
@Test
public void 사용자가저장돼있다면로그인() throws Exception {
//given
UserDto userDto = UserDto.builder()
.email("email1")
.password("password1")
.username("username1")
.phoneNumber("phoneNumber1")
.role(Role.USER)
// .birthday(LocalDate.of(2002,6,23))
.build();
//when
when(authService.login(userDto.getEmail(), userDto.getPassword())).thenReturn(userDto);
when(jwtTokenProvider.createJwtCookie(userDto.getEmail(),userDto.getRole())).thenReturn(new Cookie("test","test"));
//then
mvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(userDto))
)
.andExpect(status().isOk());
}
}
7. 출처(4. - 기반 설명)
<설명 - Application Context>
- 프로젝트를 실행하면서 만들어지는 스프링 빈은 테스트 코드에서 재사용된다.
https://docs.spring.io/spring-framework/reference/testing/integration.html#testing-ctx-management
By default, once loaded, the configured ApplicationContext is reused for each test. Thus, the setup cost is incurred only once per test suite, and subsequent test execution is much faster. In this context, the term “test suite” means all tests run in the same JVM — for example, all tests run from an Ant, Maven, or Gradle build for a given project or module. In the unlikely case that a test corrupts the application context and requires reloading (for example, by modifying a bean definition or the state of an application object) the TestContext framework can be configured to reload the configuration and rebuild the application context before executing the next test.
- TestContext는 ApplicationContext에 의해서 스프링 빈을 사용 가능
Dependency Injection of Test Fixtures When the TestContext framework loads your application context, it can optionally configure instances of your test classes by using Dependency Injection. This provides a convenient mechanism for setting up test fixtures by using preconfigured beans from your application context. A strong benefit here is that you can reuse application contexts across various testing scenarios (for example, for configuring Spring-managed object graphs, transactional proxies, DataSource instances, and others), thus avoiding the need to duplicate complex test fixture setup for individual test cases.
As an example, consider a scenario where we have a class (HibernateTitleRepository) that implements data access logic for a Title domain entity. We want to write integration tests that test the following areas:
The Spring configuration: Basically, is everything related to the configuration of the HibernateTitleRepository bean correct and present?
The Hibernate mapping file configuration: Is everything mapped correctly and are the correct lazy-loading settings in place?
The logic of the HibernateTitleRepository: Does the configured instance of this class perform as anticipated?
See dependency injection of test fixtures with the TestContext framework.
- 스프링 빈 내용이 변경되지 않을 시, ApplicationContext에서 재사용
or example, if TestClassA specifies {"app-config.xml", "test-config.xml"} for the locations (or value) attribute of @ContextConfiguration, the TestContext framework loads the corresponding ApplicationContext and stores it in a static context cache under a key that is based solely on those locations. So, if TestClassB also defines {"app-config.xml", "test-config.xml"} for its locations (either explicitly or implicitly through inheritance) but does not define @WebAppConfiguration, a different ContextLoader, different active profiles, different context initializers, different test property sources, or a different parent context, then the same ApplicationContext is shared by both test classes. This means that the setup cost for loading an application context is incurred only once (per test suite), and subsequent test execution is much faster.
'스프링' 카테고리의 다른 글
스프링 - GCS 이미지 저장/덮어쓰기/삭제 (0) | 2025.02.03 |
---|---|
JWT_사용자 정의 로그인 (0) | 2025.01.31 |