Spring Security를 사용하는 애플리케이션에서 테스트를 작성할 때, 인증된 사용자 컨텍스트를 사용해야 하는 경우가 있습니다. Spring Security는 이를 위해 다음과 같은 어노테이션을 지원합니다.
@WithMockUser
@WithUserDetails
@WithSecurityContext
@WithMockUser
@WithMockUser
는
Spring Security 테스트를 위한 가장 기본적인 어노테이션입니다.
@Test
@WithMockUser
public void testSecuredMethod() {
// 테스트 코드
}
특징
- 실제 사용자 데이터 없이도 테스트가 가능합니다.
- 기본적으로 필드는 다음과 같이 설정됩니다.
username
: "user"password
: "password"role
: "ROLE_USER"
다음과 같이, username, password, roles을 커스터마이징할 수 있습니다.
@Test
@WithMockUser(username = "admin", roles = {"USER", "ADMIN"})
public void testAdminAccess() {
// 테스트 코드
}
- 결과적으로
UsernamePasswordAuthenticationToken
타입의 Authentication 객체가 SecurityContext에 생성됩니다.
실제로, @WithMockUser
의 코드를 살펴보면 다음과 같이 WithMockUserSecurityContextFactory
클래스를 사용하고 있는 것을 확인할 수 있습니다.


해당 클래스에서 SecurityContext를 생성해 반환하는 것을 확인할 수 있습니다.
주의사항
@WithMockUser
는 Spring Security의 User 객체를 사용합니다(위 사진에서 밑줄 친 부분의User
). 따라서, UserDetails를 직접 구현하는 Custom User 객체를 사용하는 경우, 이를 사용할 수 없습니다.
따라서, @WithMockUser
어노테이션은 간단한 인증 시나리오 테스트에는 적합하지만, 복잡한 인증 로직이나 커스텀 UserDetails를 사용하는 경우에는 사용에 한계가 존재합니다.
@WithUserDetails
@WithUserDetails
는
Spring Security 테스트에서 실제 UserDetailsService
를 사용해 인증된 사용자 컨텍스트를 시뮬레이션하는 어노테이션입니다.
따라서, @WithMockUser
보다 더 실제 환경에 가까운 테스트가 가능합니다.
@Test
@WithUserDetails("testuser@example.com")
public void testUserAccess() {
// 테스트 코드
}
특징
- 실제
UserDetailsSerivce
구현체를 사용해UserDetails
객체를 로드합니다. - DB에 저장된 실제 사용자 정보로 테스트가 가능합니다.
- 기본적으로 "user"라는 이름의 사용자를 찾습니다.
- 사용자 이름을 직접 지정할 수 있고, 필요한 경우 사용할
UserDetailsService
Bean의 이름도 지정할 수 있습니다.
- 사용자 이름을 직접 지정할 수 있고, 필요한 경우 사용할
@Test
@WithUserDetails(value = "admin@example.com", userDetailsServiceBeanName = "customUserDetailsService")
public void testAdminAccess() {
// 테스트 코드
}
실제, 구현된 코드를 확인해보면 다음과 같습니다.

마찬가지로 WithSecurityContextFactory
를 구현한 WithUserDetailsSecurityContextFactory
를 사용하고 있는 것을 확인할 수 있습니다.


WithUserDetailsSecurityContextFactory
의 createSecurityContext
메서드를 확인해보면 위와 같습니다. 내부 로직에서 실제로 UserDetailsService
와 이를 통해 UserDetails
객체를 가져오는 것을 확인할 수 있습니다.
주의사항
UserDetailsService
구현이 필요합니다.- 테스트용 사용자 데이터가 미리 준비되어 있어야 합니다.
- 테스트 실행 전에 지정된 사용자가 DB에 존재해야 합니다.
결국, 실제 DB에 저장된 사용자의 데이터를 사용하는 방식이기 때문에, 여러 가지로 고려해야 할 점이 많은 테스트 방식입니다.
@WithSecurityContext
@WithSecurityContext
는 가장 유연한 방식으로,
직접 SecurityContext를 생성할 수 있게 해주는 어노테이션입니다.
특징
- 실제
UserDetails
객체를 사용할 수 있습니다. - 완전한 커스터마이징이 가능합니다.
- 복잡한 인증 시나리오를 테스트할 수 있습니다.
- 실제 애플리케이션의 보안 로직을 정확하게 테스트할 수 있습니다.
쉽게 말해, 위의 @WithMockUser
와 @WithUserDetails
어노테이션이 사용한 WithSecurityContextFactory
를 직접 구현하는 방식입니다.
구현 순서
구현에 들어가기 앞서, 현재 애플리케이션에서 구현된 사용자 관련 부분의 코드는 다음과 같습니다.
User.class
@Entity
@Getter
@NoArgsConstructor
@Table(name = "users")
public class User extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@Column(nullable = false)
@Enumerated(value = EnumType.STRING)
private UserRoleEnum role;
public User(String username, String password, String email, UserRoleEnum role) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
}
}
UserDetailsImpl.class
@RequiredArgsConstructor
public class UserDetailsImpl implements UserDetails {
private final User user;
public User getUser() {
return user;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
UserRoleEnum role = user.getRole();
String authority = role.getAuthority();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleGrantedAuthority);
return authorities;
}
}
UserDetailsService.class
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));
return new UserDetailsImpl(user);
}
}
@WithSecurityContext
를 사용하기 위한 과정은 다음과 같습니다.
1. 커스텀 어노테이션 생성
애플리케이션에 구현된 사용자 엔티티의 형식에 맞게 테스트에서 사용할 Mock 사용자 어노테이션을 생성합니다.
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithSecurityContextFactoryImpl.class)
public @interface MockUser {
long id() default 1L;
String username() default "mockUser";
String password() default "password";
String email() default "mock@email.com";
UserRoleEnum role() default UserRoleEnum.USER;
}
2. SecurityContextFactory 구현
위에서 생성한 커스텀 어노테이션이 사용할 SecurityContextFactory를 구현합니다.
public class WithSecurityContextFactoryImpl implements WithSecurityContextFactory<MockUser> {
@Override
public SecurityContext createSecurityContext(MockUser annotation) {
User mockUser = new User(annotation.username(), annotation.password(), annotation.email(), annotation.role());
mockUser.setId(annotation.id());
UserDetailsImpl userDetails = new UserDetailsImpl(mockUser);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
userDetails, userDetails.getPassword(), userDetails.getAuthorities()
);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(token);
return context;
}
}
위 코드에 보이는 것처럼, @WithMockUser
, @WithUserDetails
와는 다르게 직접 구현한 UserDetailsImpl
를 사용해 SecurityContext를 만들어 반환하는 것을 확인할 수 있습니다.
3. 테스트에 어노테이션 적용
이제 만들어진 어노테이션을 Spring Security 테스트에 활용할 수 있습니다.
@Test
@MockUser
void testWithMockCustomUser() throws Exception {
// Given
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserDetailsImpl userDetails = (UserDetailsImpl)authentication.getPrincipal();
User user = userDetails.getUser();
...
}
이처럼, @WithSecurityContext
를 사용하면 테스트 코드를 더욱 유연하고 강력하게 만들 수 있으며, 실제 애플리케이션의 보안 로직을 정확하게 테스트할 수 있습니다.