인프런 커뮤니티 질문&답변

비가싫어요님의 프로필 이미지

작성한 질문수

Practical Testing: 실용적인 테스트 가이드

Spring Security 를 포함한 WebMvcTest 질문드립니다.

23.07.30 11:35 작성

·

1.2K

·

수정됨

0

안녕하세요 좋은 강의 덕분에 테스트에 대해 많이 알게 되었습니다 :)

스프링 시큐리티를 포함한 테스트와 관련해서 질문을 드려보고자 글을 쓰게 되었습니다. (강의에서는 스프링 시큐리티를 사용하지 않지만 테스트와 관련해서 물어볼 곳이 마땅치 않아 이 곳에 글을 남기는 점 양해해주시면 감사하겠습니다)

@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
@Slf4j
public class AuthController {

    private final AuthService authService;

    @PostMapping("/auth/sign-up")
    public String signUp(@RequestBody SignUpRequest request) {
       authService.signUp(request);
       return "회원가입 성공";
    }
}

현재 회원가입을 하는 컨트롤러의 메서드는 다음과 같습니다.

 

@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class AuthService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    @Transactional
    public void signUp(SignUpRequest request) {

       if (userRepository.findByEmail(request.getEmail()).isPresent()) {
          throw new AlreadyExistException("이미 존재하는 이메일입니다.");
       }

       if (userRepository.findByNickname(request.getNickname()).isPresent()) {
          throw new AlreadyExistException("이미 존재하는 닉네임입니다.");
       }

       User user = User.builder()
          .name(request.getName())
          .email(request.getEmail())
          .password(request.getPassword())
          .nickname(request.getNickname())
          .role(UserRole.USER)
          .build();

       user.passwordEncode(passwordEncoder);
       userRepository.save(user);
    }
}

그리고 실제 회원가입이 진행되는 로직은 이렇습니다.

 

그리고 제가 WebMvcTest 어노테이션을 사용해서 테스트 코드를 작성한 건 다음과 같습니다.

@ActiveProfiles("test")
@AutoConfigureMockMvc
@WebMvcTest(AuthController.class)
class AuthControllerTestWithWebMvcTest {

    @Autowired
    protected MockMvc mockMvc;

    @Autowired
    protected ObjectMapper objectMapper;

    @MockBean
    private AuthService authService;

    @MockBean
    private UserRepository userRepository;


    @DisplayName("회원가입 한다.")
    @CustomMockUser // 회원가입을 해야되는데 인증이 필요하다? -> 논리적으로 말이 안 됨 -> 올바른 테스트?
    @Test
    void signUp() throws Exception {
       // given
       SignUpRequest request = SignUpRequest.builder()
          .name("zun")
          .email("zun@test.com")
          .password("12345")
          .nickname("zunny")
          .build();

       // then
       mockMvc.perform(post("/api/auth/sign-up")
             .content(objectMapper.writeValueAsString(request))
             .contentType(APPLICATION_JSON)
             .with(csrf())
          )
          .andExpect(status().isOk())
          .andDo(print());
    }

}

보시다시피 회원가입 API에 요청이 제대로 되는지를 확인하기 위해 @CustomMockUser 라는 어노테이션을 사용하고 있습니다. (이 어노테이션은 테스트에서 인증을 처리하기 위해 만든 어노테이션입니다. 호돌맨님 강의를 참고해서 만들었습니다.)

제가 혼란스러운 부분은 테스트코드에 써놓은 주석처럼 회원가입이 제대로 되는지를 테스트하기 위한 컨트롤러 코드인데 이걸 통과시키기 위해 인증을 담당하는 @CustomMockUser를 사용하는 것이 올바른 테스트인가 하는 의문입니다.

@WebMvcTest를 사용하지 않고 @SpringBootTest를 사용하면 다음처럼 할 수가 있습니다.

@ActiveProfiles("test")
@AutoConfigureMockMvc
@SpringBootTest
class AuthControllerTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    void tearDown() {
       userRepository.deleteAllInBatch();
    }

    @DisplayName("회원가입 한다.")
    @Test
    void signUp() throws Exception {
       //given
       SignUpRequest request = SignUpRequest.builder()
          .name("zun")
          .email("zun@test.com")
          .password("12345")
          .nickname("zunny")
          .build();

       //expected
       mockMvc.perform(post("/api/auth/sign-up")
             .content(objectMapper.writeValueAsString(request))
             .contentType(APPLICATION_JSON)
          )
          .andExpect(status().isOk())
          .andDo(print());
    }

}

 

혹시 이와 관련해서 우빈님은 어떻게 생각하시는지, 그리고 스프링 시큐리티를 사용하는 프로젝트에서는 컨트롤러 쪽 테스트를 어떻게 하는 것이 좋은지 알려주시면 감사하겠습니다!

답변 2

1

인프런 AI 인턴님의 프로필 이미지

2023. 08. 01. 11:41

안녕하세요, 인프런 AI 인턴이에요.
스프링 시큐리티를 사용한 테스트 관련 질문이시군요. 저도 최대한 도움을 드리겠습니다.

먼저, 스프링 시큐리티를 포함한 WebMvcTest를 하기 위해서는 보통 테스트 Configuration 클래스를 작성하거나, 특정 설정을 추가해야 합니다.
예를 들어, @WebMvcTest 애노테이션과 함께 @Import(SecurityConfig.class)를 사용하여 스프링 시큐리티 설정 클래스를 import할 수 있습니다. 이렇게 하면 테스트 시에 스프링 시큐리티의 설정이 적용되게 됩니다.

그리고 테스트 메서드에서는 인증된 유저로 테스트를 진행하기 위해 SecurityContextHolder를 사용하여 인증 정보를 설정할 수 있습니다. 예를 들어, @WithUserDetails 애노테이션을 사용하여 특정 유저로 인증된 상태에서 테스트를 수행할 수 있습니다.

테스트 코드에서 특정 컨트롤러의 메서드를 호출할 때는 MockMvc를 사용합니다. MockMvc는 서블릿 컨테이너를 실행하지 않고도 스프링 MVC의 동작을 모방하여 테스트할 수 있는 클래스입니다. 따라서 MockMvc를 사용하여 회원가입 컨트롤러의 메서드를 호출하고 응답을 확인할 수 있습니다.

또한, 테스트에서는 사용자가 로그인 상태인지, 권한이 있는지 등을 확인하기 위해 SecurityContext를 확인할 수 있는 방법이 필요할 수 있습니다. 이 경우에는 SecurityContextHolder를 이용하여 SecurityContext에 접근한 뒤 필요한 정보를 확인하면 됩니다.

위의 내용을 참고하여 여러분이 원하는 스프링 시큐리티를 포함한 WebMvcTest를 작성해 보세요. 추가로 궁금한 점이 있으시면 언제든지 물어보세요! :)

0

박우빈님의 프로필 이미지
박우빈
지식공유자

2023. 08. 06. 15:56

안녕하세요, JUNN님! :)

아하 네 적용하신 시큐리티 내용이 어떻게 되는지 정확히는 모르겠지만, 보통은 회원가입과 로그인 페이지는 모든 사용자가 접근 가능하도록 만들기 때문에, 시큐리티 설정에서 해당 API를 열어주어야 할 것 같아요.

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {	
        httpSecurity
            .authorizeRequests()
                .antMatchers("/login", "/signup").permitAll()
                .anyRequest().authenticated();

        return http.build();
    }

}

간단하게 보면 위와 같은 형태일 것 같네요.
해당 API를 열어주면 테스트에서도 MockUser가 필요없지 않을까 싶습니다. ㅎㅎ

감사합니다. :)

비가싫어요님의 프로필 이미지

2023. 08. 06. 20:04

수업 내용과 상관없는 질문인데도 답변해주셔서 감사드립니다 우빈님 :)

@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		return http
			.csrf(AbstractHttpConfigurer::disable)
			.authorizeHttpRequests()
			.anyRequest().permitAll()
			.and()
			.logout()
			.logoutUrl("/api/auth/logout")
			.logoutSuccessUrl("/")
			.invalidateHttpSession(true)
			.and()
			.addFilterBefore(customEmailPasswordFilter(), UsernamePasswordAuthenticationFilter.class)
			.exceptionHandling(e -> {
				e.accessDeniedHandler(new Http403Handler(objectMapper));
				e.authenticationEntryPoint(new Http401Handler(objectMapper));
			})
			.build();
	}


혹시 이렇게 적용이 되어있는 상황인데 anyRequest().permitAll() 이렇게 처리를 해주고 인증이나 인가가 필요한 부분은 @PreAuthorize를 해주고 있는 상황인데 이런 경우에도 회원가입 요청 URL을 따로 열어줘야 할까요?