프로필 수정 테스트

yuri·2020년 11월 12일

이전에 해왔던 test와 약간 다르다. 이전은 회원 가입 위주였기 때문에 인증이 되지 않은 사용자에게 하는 것이였고, 지금은 인증된 사용자에 한에서 프로필 정보를 수정하는 것이다.

인증된 사용자가 접근할 수 있는 기능 테스트

  • 실제 db에 저장되어 있는 정보에 대응하는 인증된 authentication이 필요
  • WithMockUser로는 처리 불가

url 사용하기 쉽도록 default 값으로 변경

SettingsController.java

    static final String SETTINGS_PROFILE_VIEW_NAME = "settings/profile";
    static final String SETTINGS_PROFILE_URL = "/settings/profile";

static은 알다시피 클래스 변수이다. 그러므로 static final은 객체(인스턴스)가 아닌 클래스에 존재하는 단 하나의 상수이다. 즉 객체마다 값이 바뀌는 것이 아닌 클래스에 존재하는 상수이므로 선언과 동시에 초기화를 해 주어야하는 클래스 상수이다.

Test code 작성 (3가지)

  • 프로필 수정 폼
  • 프로필 수정하기 - 입력값 정상
  • 프로필 수정하기 - 입력값 에러

요청을 보낼때 어떤 유저가 보내는지 설정을 해야 하는데 그걸 어떻게 해야 할까?

    @WithAccount("keesun")
    @DisplayName("프로필 수정하기 - 입력값 정상")
    @Test
    void updateProfile() throws Exception {
        String bio = "짧은 소개를 수정하는 경우!";
        mockMvc.perform(post(SettingsController.SETTINGS_PROFILE_URL)
                .param("bio", bio)
                .with(csrf()))
                .andExpect(status().is3xxRedirection())
                .andExpect(redirectedUrl(SettingsController.SETTINGS_PROFILE_URL))
                .andExpect(flash().attributeExists("message"));

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

인증된 사용자를 제공할 커스텀 에노테이션 만들기

@WithMockUser?

  • The user with the username "user" does not have to exist since we are mocking the user
  • The Authentication that is populated in the SecurityContext is of type UsernamePasswordAuthenticationToken
  • The principal on the Authentication is Spring Security’s User object
  • The User will have the username of "user", the password "password", and a single GrantedAuthority named "ROLE_USER" is used.
@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
    String message = messageService.getMessage();
    ...
}

보통 위의 방법을 사용한다. 하지만 현재 우리는 이것을 사용할 수 없다. 왜 ? 위 어플리케이션에는 진짜 데이터 베이스에 들어있는 데이터, 그 유저가 컨텍스트에 들어있어야 한다.

@BeforeEach
void beforEach() {
	SignUpForm signUpForm = new SignUpForm();
    signUpForm.setNickname("yuri");
    signUpForm.setEmail("yuri@naver.com");
    signUpForm.set

왠지 @BeforeEach 를 사용하면 될 것 같은데 .. 안된다. 이 어노테이션 전에 이미 @WithAccount("keesun") 가 실행된다. 스프링 시큐리티 중에 실행하는 위치를 설정할 수도 있다.

@WithAccount(value = "keesun", setupBefore = TestExecutionEvent.TEST_EXECUTION)

Before 다음, test code 바로 직전에 실행하라는 뜻인데 버그가 있다. 이게 제대로 동작하지 않는다. 데이터를 넣기 전에 기존에 해당하는 유저 정보를 가져오다가 실패한다. UserDetails에는 keesun 이 없기 때문에 에러가 발생한다. 스프링 시큐리티 테스트 기능이 junit 제대로 동작하지 않는다. 나중에 고쳐지기를 바라며 ... 그래서 다른 것을 사용할 것이다.

@WithSecurityContext

따라서 @WithSecurityContext를 활용하여 확장을 할 수 있는 기능을 사용하자.

커스텀 애노테이션 생성

WithAccount.java 라는 커스텀한 애노테이션를 만든다. SecurityContext에다가 factory 애트리뷰트를 사용해서 SecurityContext를 만들어줄 factory를 만들 것이다.

커스텀 어노테이션을 생성하려면

@interface [어노테이션 명]이라는 형태로 어노테이션을 만들면 된다. 어노테이션은 멤버를 가질 수 있으며 타입과 이름, 디폴트값을 설정할 수 있다. 디폴트값을 따로 지정해주지 않으면 기본 엘리멘트가 된다.

※ 엘리멘트 뒤에는 ( ) 괄호를 붙여야 한다.

SecurityContextFactory 구현

WithAccountSecurityContextFactory.java 생성한다. 이것은 WithSecurityContextFactory를 확장해서 만든다. WithSecurityContextFactory를 구현해야 한다.

Q. 왜 자바의 인터페이스를 사용할까?

완벽한 추상화를 달성하기 위함. 인터페이스를 사용함으로써, 다중상속의 기능을 지원할 수 있다.

WithSecurityContextFactory 타입에 방금 만든 WithAccount 를 넣어준다.

@RequiredArgsConstructor
public class WithAccountSecurityContextFactory implements WithSecurityContextFactory<WithAccount> {

    private final AccountService accountService;

    @Override
    public SecurityContext createSecurityContext(WithAccount withAccount) {
        String nickname = withAccount.value();

        SignUpForm signUpForm = new SignUpForm();
        signUpForm.setNickname(nickname);
        signUpForm.setEmail(nickname + "@email.com");
        signUpForm.setPassword("12345678");
        accountService.processNewAccount(signUpForm);

        UserDetails principal = accountService.loadUserByUsername(nickname);
        Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities());
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authentication);
        return context;
    }
}

그럼 이런 메소드를 등록해야 한다. 이 클래스는 빈으로 등록되어있기 때문에 필요한 것들을 주입받을 수 있다. AccountService 을 주입 받아서 사용하자.

원래는 시큐리티 컨텍스트만 만들어서 리턴하면 된다.

value()를 해서 닉네임을 받아오자. 받아온 다음에 해당되는 데이터를 읽어서 시큐리티 컨텍스트에 넣어준다.

여기서 주의할 점!

WithAccount를 사용할 때마다 그 account에 해당하는 계정을 만들기 때문에 매번 테스트를 사용한 다음에는 지워줘야 한다.

SettubgsController.java

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

그럼 withAccount를 여러번 사용해도 문제가 없다.

이후 테스트를 실행하면 기선이라는 account를 만들고 기선이라는 account를 스프링 시큐리티 컨텍스트에 넣은 다음에 테스트를 실행하게 된다. 따라서 성공하게 됨.

@WithAccount("keesun") 어노테이션은 상당히 편리하다.

참고

@Retention: 자바 컴파일러가 어노테이션을 다루는 방법을 기술하며, 특정 시점까지 영향을 미치는지를 결정한다.

  • RetentionPolicy.SOURCE : 컴파일 전까지만 유효. (컴파일 이후에는 사라짐)
  • RetentionPolicy.CLASS : 컴파일러가 클래스를 참조할 때까지 유효.
  • RetentionPolicy.RUNTIME : 컴파일 이후에도 JVM에 의해 계속 참조가 가능. (리플렉션 사용)

출처 : 인프런 백기선님의 스프링과 JPA 기반 웹 애플리케이션 개발

https://elfinlas.github.io/2017/12/14/java-annotation/
https://m.blog.naver.com/PostView.nhn?blogId=goddlaek&logNo=220889229659&proxyReferer=https:%2F%2Fwww.google.com%2F
https://joochang.tistory.com/76
https://coding-factory.tistory.com/575

profile
Step by step goes a long way ✨

0개의 댓글