프로필 수정 테스트

Yuri Lee·2020년 11월 12일
0

이전에 해왔던 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개의 댓글