java.lang.AssertionError: No value at JSON path "$.userId"
at org.springframework.test.util.JsonPathExpectationsHelper.evaluateJsonPath(JsonPathExpectationsHelper.java:351)
at org.springframework.test.util.JsonPathExpectationsHelper.assertValue(JsonPathExpectationsHelper.java:148)
at org.springframework.test.web.servlet.result.JsonPathResultMatchers.lambda$value$2(JsonPathResultMatchers.java:112)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:214)
at com.mini_prioject.display_board.controller.UserControllerTest.createUser_WhenValidRequest(UserControllerTest.java:81)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
✍️원인
Mockito는 같은 객체(request)가 전달될 때만 응답을 반환
하지만 mockMvc에서 보낸 JSON 요청을 Spring이 다시 객체로 만들면, 원래 request와 다른 객체로 인식됨
java.lang.AssertionError: Status expected:<201> but was:<401>
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:61)
at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:128)
at org.springframework.test.web.servlet.result.StatusResultMatchers.lambda$matcher$9(StatusResultMatchers.java:640)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:214)
at com.mini_prioject.display_board.controller.UserControllerTest.createUserSuccessTest(UserControllerTest.java:77)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
✍️원인
By default, tests annotated with @WebMvcTest will also auto-configure Spring Security @WebMvcTest 사용 시, 스프링 시큐리티가 자동으로 구성하는 Configuration 파일들을 불러와서 사용
자동으로 구성되는 SpringBootWebSecurityConfiguration이 권한을 요청하지만 인증 권한을 가진 사용자로 테스트하지 않아 오류 발생
// 테스트 User를 반환하는 로직
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return User.builder()
.username("testUser")
.password(new BCryptPasswordEncoder().encode("password123"))
.roles("USER")
.build();
}
}
// 실제 DB에 저장된 User를 반환하는 로직
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username)
.map(user -> User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRole())
.build())
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
}
@WebMvcTest(UserController.class)
@Import(SecurityConfig.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@DisplayName("Principal 내부 값 테스트")
@WithUserDetails(value = "testUser")
void testWithUserDetails() throws Exception {
mockMvc.perform(get("/api/v1/profile"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("testUser"));
}
}
@WithMockUser - 인증된 사용자 @WithAnonymousUser - 미인증 사용자 (principal에서 "anonymous"가 들어가있음) @WithUserDetails - 메서드가 principal 내부의 값을 직접 사용하는 경우 (별도의 사전 설정 필요) - WithUserDetails를 사용하면 UserDetailsService가 동작해서 Principal을 설정해줌 - loadUserByUsername()이 직접 UserDetails를 반환
java.lang.AssertionError: Status expected:<201> but was:<403>
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:61)
at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:128)
at org.springframework.test.web.servlet.result.StatusResultMatchers.lambda$matcher$9(StatusResultMatchers.java:640)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:214)
at com.mini_prioject.display_board.controller.UserControllerTest.createUserSuccessTest(UserControllerTest.java:60)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
✍️원인
@WebMvcTest 또는 MockMvc를 사용할 때는 Spring Security가 자동으로 CSRF 검사를 활성화
따라서 CSRF 토큰이 없는 요청은 403 Forbidden 오류가 발생할 수 있음
CSRF(Cross-Site Request Forgery)
- 공격자가 악의적인 코드를 심어놓은 사이트를 만들어놓고, 로그인 된 사용자가 클릭하게 만들어사용자 의지와 무관한 요청을 발생시키는 공격
-사용자는 로그인 한 상태고 쿠키, 권한을 갖고있기 때문에 공격자가 위조한 웹사이트에 방문하게 되면 사용자 모르게 악의적인 POST, DELETE 요청을 정상 수행하도록 만들어버리는 공격
- 이를 해결하기 위해 스프링 시큐리티에서는 "CSRF 토큰" 을 이용해 토큰 값을 비교해서 일치하는 경우에만 메서드를 처리하도록 만든다.
@SpringBootTest @Controller, @Service, @Component, @Bean 등등 모두 다 불러와서 실제 환경과 동일하게 테스트 하고자 할 때 사용
@WebMvcTest Using this annotation will disable full auto-configuration and instead apply only configuration relevant to MVC tests (i.e.@Controller, @ControllerAdvice, @JsonComponent, Converter/GenericConverter, Filter, WebMvcConfigurer and HandlerMethodArgumentResolver beans but not @Component, @Service or @Repository beans).
By default, tests annotated with @WebMvcTest will also auto-configure Spring Security and MockMvc
@MockitoBean으로 주입해준 UserService 빈은 #0이 뒤에 붙어서 스프링 빈으로 저장 UserService 빈은 MockitoMock을 통해 반들어져있고 내부는 다 null로 채워져있으므로,텅 비어있는 userService 빈을 실제 동작하는 것처럼 흉내내기 위해서 Mocking(given)을 설정
Creates a RequestPostProcessor that will automatically populate a valid CsrfToken in the request. Returns: the SecurityMockMvcRequestPostProcessors.CsrfRequestPostProcessor for further customizations.
Caused by:
org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'jpaAuditingHandler':
Cannot resolve reference to bean 'jpaMappingContext' while setting constructor argument;
nested exception is org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'jpaMappingContext':
Invocation of init method failed;
nested exception is java.lang.IllegalArgumentException:
JPA metamodel must not be empty!
✍️원인
jpaAuditing 기능을 사용하기 위해 @EnableJPaAuditing을 Application에 선언
Spring 테스트에서 컨테이너를 사용하는 경우, 기본 애플리케이션 클래스(@SpringBootApplication이 붙은 클래스)를 항상 로드
Application 클래스에 @EnableJpaAuditing이 설정되어 있어서, 모든 테스트에서 JPA 관련 빈(EntityManager, JpaRepository 등)이 필요하게 됨
통합 테스트(@SpringBootTest)는 JPA 관련 빈을 전부 로드하므로 문제 없지만, @WebMvcTest는 컨트롤러 관련 빈만 로드하므로 JPA 빈이 없어서 오류 발생함
✅해결
1. @MokitoBean 추가
@Test
@DisplayName("회원가입 성공 테스트")
@MockitoBean(JpaMetamodelMappingContext.class)
void createUserSuccessTest() throws Exception {}
각각의 Test에 @MokitoBean을 선언해야하는 번거러움 존재
2. JpaAuditingConfig 파일 분리
@EnableJpaAuditing
@Configuration
public class JpaAuditingConfig {
}