스프링과 JPA 기반 웹 애플리케이션 개발 #30 프로필 수정 테스트

Jake Seo·2021년 6월 3일
0

스프링과 JPA 기반 웹 애플리케이션 개발 #30 프로필 수정 테스트

해당 내용은 인프런, 스프링과 JPA 기반 웹 애플리케이션 개발의 강의 내용을 바탕으로 작성된 내용입니다.

강의를 학습하며 요약한 내용을 출처를 표기하고 블로깅 또는 문서로 공개하는 것을 허용합니다 라는 원칙 하에 요약 내용을 공개합니다. 출처는 위에 언급되어있듯, 인프런, 스프링과 JPA 기반 웹 애플리케이션 개발입니다.

제가 학습한 소스코드는 https://github.com/n00nietzsche/jakestudy_webapp 에 지속적으로 업로드 됩니다. 매 커밋 메세지에 강의의 어디 부분까지 진행됐는지 기록해놓겠습니다.


프로필 수정 테스트

  • 인증된 사용자가 접근할 수 있는 기능 테스트하기
    • 실제 DB에 저장되어 있는 정보에 대응하는 인증된 Authentication이 필요하다.
    • @WithMockUser로는 처리할 수 없다.
  • 인증된 사용자를 제공할 커스텀 애노테이션 만들기
    • @WithAccount
  • @WithAccount 커스텀 애노테이션
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithAccountSecurityContextFactory.class)
public @interface WithAccount {
  String value();
}
  • SecurityContextFactory 구현
public class WithAccountSecurityContextFacotry implements WithSecurityContextFactory<WithAccount> {
  // 빈을 주입받을 수 있다.
  // Authentication 만들고 SecurityContext에 넣어주기
  UserDetails principal = accountService.loadUserByUsername(nickname);
  Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities());
  SecurityContext context = SecurityContextHolder.createEmptyContext();
  context.setAuthentication(authentication);
}

프로필 수정 테스트의 특징

  • 스프링 시큐리티 관련 테스트
  • 이번엔 진짜 DB 에서 User 가 바뀌었는지 확인해야 한다.
  • @WithMockUser 로는 확인할 수 없다.
    • DB 에 없는 데이터로 그냥 principal만 넣어서 있는 척 하는 거기 때문.
  • @WithUserDetails 로도 확인할 수 없다.
    • @BeforeEach 전에 @WithUserDetails가 먼저 실행돼서 유저를 찾지 못한다.
    • 사실 setupBefore 라는 엘리먼트 값이 있지만, 현재 Junit5에서 버그로 인하여 적용이 잘 안됨 (스프링부트 2.4.2에는 패치됐다는 이야기가 있음)
  • @WithSecurityContext 애노테이션을 이용할 것임.

WithAccount 애노테이션 작성

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithAccountSecurityContextFactory.class)
public @interface WithAccount {
    String nickname();
}

nickname을 받고, 팩토리를 이용해서 SecurityContextAuthentication 정보를 넣어줄 것임.

WithAccountSecurityContextFactory 클래스 작성

@RequiredArgsConstructor
// 제네릭 타입에는 애노테이션 타입을 주면 된다.
// 이 클래스는 빈으로 등록되기 때문에 얼마든지 빈을 주입받을 수 있다.
public class WithAccountSecurityContextFactory implements WithSecurityContextFactory<WithAccount> {

    private final AccountService accountService;

    @Override
    public SecurityContext createSecurityContext(WithAccount withAccount) {
        String nickname = withAccount.nickname();
        String password = "test1234";
        String email = nickname + "@test.com";

        // 회원 가입
        SignUpForm signUpForm = new SignUpForm();
        signUpForm.setEmail(email);
        signUpForm.setPassword(password);
        signUpForm.setNickname(nickname);
        accountService.signUpAndSendEmail(signUpForm);

        // 이게 원래 `@UserDetails` 애노테이션이 하던 일이다.
        // Authentication 만들기 및 SecurityContext 에 넣어주기
        UserDetails principal = accountService.loadUserByUsername(nickname);
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities());
        // 빈 시큐리티 컨텍스트 만들고, 거기에 AuthenticationToken 넣기
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authenticationToken);

        return context;
    }
}

WithSecurityContextFactory<WithAccount>의 구현인 createSecurityContext() 메소드를 작성해준 부분이다.

반환 값은 SecurityContext의 구현체이며, SeucirtyContext 객체를 만드는 방법은 SecurityContextHolder()strategy에 따라 다른데 MODE_THREADLOCAL이 기본 전략이며, SecurityContextImpl() 생성자를 활용한다.

SecurityContextFactory라는 이름답게, SecurityContext를 만들고 내부에 Authentication을 세팅해주고 끝난다. Authentication을 세팅할 때는 AuthenticationToken을 생성하여 넣어주면 된다.

테스트 작성

@SpringBootTest
@AutoConfigureMockMvc
class SettingsControllerTest {
    @Autowired MockMvc mockMvc;
    @Autowired AccountRepository accountRepository;

    @BeforeEach
    public void beforeEach() { }
    @AfterEach
    public void afterEach() {
        accountRepository.deleteAll();
    }

    /**
     * 스프링 시큐리티 관련 테스트
     * 이번엔 진짜 DB 에서 User 가 바뀌었는지 확인해야 하기 때문에
     * @WithMockUser 로는 확인할 수 없다.
     *   - DB 에 없는 데이터로 그냥 principal만 넣어서 있는 척 하는 거기 때문.
     * @WithUserDetails 로도 확인할 수 없다.
     *   - @BeforeEach 전에 @WithUserDetails가 먼저 실행돼서 유저를 찾지 못한다.
     *   - 사실 setupBefore 라는 엘리먼트 값이 있지만, 현재 Junit5에서 버그로 인하여 적용이 잘 안됨
     * @WithSecurityContext 애노테이션을 이용할 것임.
     * https://docs.spring.io/spring-security/site/docs/current/reference/html5/#test
     */
    @DisplayName("프로필 수정하기 - 입력값 정상")
    @WithAccount(nickname = "jake")
    @Test
    public void updateProfile() throws Exception {
        String settingsProfileURL = "/settings/profile";
        String bio = "짧은 소개";
        mockMvc.perform(post(settingsProfileURL)
                .param("bio", bio)
                .with(csrf()))
                .andExpect(status().is3xxRedirection())
                .andExpect(redirectedUrl(settingsProfileURL))
                .andExpect(flash().attributeExists("message"));

        Account loginAccount = accountRepository.findByNickname("jake");
        assertEquals(bio, loginAccount.getBio());
    }

    @DisplayName("프로필 수정하기 - 입력값 너무 길게")
    @WithAccount(nickname = "jake")
    @Test
    public void updateProfileTooLongBio() throws Exception {
        String settingsProfileURL = "/settings/profile";
        String bio = "긴소개----------------------------------------------------------------------------------------------------------------------------------";
        mockMvc.perform(post(settingsProfileURL)
                .param("bio", bio)
                .with(csrf()))
                .andExpect(status().isOk())
                .andExpect(view().name("settings/profile"))
                .andExpect(model().attributeExists("account"))
                .andExpect(model().attributeExists("profile"))
                .andExpect(model().hasErrors());

        Account loginAccount = accountRepository.findByNickname("jake");
        assertNull(loginAccount.getBio());
    }

    @DisplayName("프로필 수정하기 폼")
    @WithAccount(nickname = "jake")
    @Test
    public void updateProfileForm() throws Exception {
        String settingsProfileURL = "/settings/profile";

        mockMvc.perform(get(settingsProfileURL))
                .andExpect(status().isOk())
                .andExpect(model().attributeExists("account"))
                .andExpect(model().attributeExists("profile"));
    }
}

이전과 다르게 특별한 건 없다. @WithAccount()를 이용하여 SecurityContext와 디비에 생성된 계정을 이용하며, @AfterEach()를 통해 매번 계정은 디비에서 지워준다.

updateProfile 메소드 에러 해결

    public String updateProfile(@LoginAccount Account loginAccount,
                                // @ModelAttribute 애노테이션은 생략 가능하다.
                                // 파라미터의 순서가 @ModelAttribute 로 가져오는 것 이후에 Model이 와야 한다.
                                // 순서를 안지키면 400에러가 뜬다.
                                // BindingResult 도 Model 보다 앞에 있어야 한다.
                                // 파라미터의 순서가 영향을 미친다는 사실을 처음알았다.
                                // Model을 최대한 뒤로 빼자.
                                @ModelAttribute @Valid Profile profile,
                                Errors errors,
                                RedirectAttributes redirectAttributes,
                                Model model) {
                                ...

이건 좀 잡다한 건데, 여기서 엄청 시간을 소비했다.

Model 파라미터는 @ModelAttributeBindingResult 파라미터들보다 뒤에 와야 한다. 안그러면 @Valid 과정에서 컨트롤러 메소드로 정상적으로 진입하지 못하고 400에러 페이지를 계속 보여주게 된다.

profile
풀스택 웹개발자로 일하고 있는 Jake Seo입니다. 주로 Jake Seo라는 닉네임을 많이 씁니다. 프론트엔드: Javascript, React 백엔드: Spring Framework에 관심이 있습니다.

0개의 댓글

관련 채용 정보