코딩하는 털보

11 to 9, Day 5 본문

Diary/Eleven to Nine

11 to 9, Day 5

이정인 2021. 2. 25. 20:01

Today, ToDoList

Toy Project - NGMA

  • 테스트 코드 작성
  • 컨트롤러 리팩토링

테스트 코드 작성

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class AccountControllerTest {

    @Autowired
    private MockMvc mvc;

    @Autowired
    private AccountService accountService;

    @Autowired
    private ObjectMapper objectMapper;

    @BeforeEach
    private void setUp() {
        AccountDto account1 = new AccountDto();
        account1.setEmail("jilee@example.com");
        account1.setPassword("jilee123");
        accountService.registerAccount(account1);

        AccountDto account2 = new AccountDto();
        account2.setEmail("sjlee@example.com");
        account2.setPassword("sjlee123");
        accountService.registerAccount(account2);
    }

    @Test
    public void registerAccount() throws Exception {
        AccountDto accountDto = new AccountDto();
        accountDto.setEmail("nobody@example.com");
        accountDto.setPassword("newAccount");
        String accountDtoJson = objectMapper.writeValueAsString(accountDto);

        mvc.perform(post("/account")
                .contentType(MediaType.APPLICATION_JSON)
                .content(accountDtoJson))
                .andDo(print())
                .andExpect(status().is3xxRedirection());

        assertThat(accountService.getUserByEmail("nobody@example.com")).isNotEmpty();
    }

    @Test
    public void registerAccountWithShortPassword() throws Exception {
        AccountDto accountDto = new AccountDto();
        accountDto.setEmail("nobody@example.com");
        accountDto.setPassword("short");
        String accountDtoJson = objectMapper.writeValueAsString(accountDto);

        mvc.perform(post("/account")
                .contentType(MediaType.APPLICATION_JSON)
                .content(accountDtoJson))
                .andDo(print())
                .andExpect(status().isBadRequest())
                .andExpect(content().string("can't create account, because of password."))
                .andExpect(result -> assertTrue(result.getResolvedException() instanceof InvalidPasswordException));

        assertThat(accountService.getUserByEmail("nobody@example.com")).isEmpty();
    }

    @Test
    public void registerAccountWithoutPassword() throws Exception {
        AccountDto accountDto = new AccountDto();
        accountDto.setEmail("nobody@example.com");
        String accountDtoJson = objectMapper.writeValueAsString(accountDto);

        mvc.perform(post("/account")
                .contentType(MediaType.APPLICATION_JSON)
                .content(accountDtoJson))
                .andDo(print())
                .andExpect(status().isBadRequest())
                .andExpect(content().string("can't create account, because of password."))
                .andExpect(result -> assertTrue(result.getResolvedException() instanceof InvalidPasswordException));

        assertThat(accountService.getUserByEmail("nobody@example.com")).isEmpty();
    }

    @Test
    public void registerAccountExistEmail() throws Exception {
        AccountDto accountDto = new AccountDto();
        accountDto.setEmail("jilee@example.com");
        accountDto.setPassword("password");
        String accountDtoJson = objectMapper.writeValueAsString(accountDto);

        mvc.perform(post("/account")
                .contentType(MediaType.APPLICATION_JSON)
                .content(accountDtoJson))
                .andDo(print())
                .andExpect(status().isBadRequest())
                .andExpect(content().string("can't create account, because of used email."))
                .andExpect(result -> assertTrue(result.getResolvedException() instanceof UsedEmailException));

        assertThat(accountService.getUserByEmail("jilee@example.com")).isNotEmpty();
    }

    @Test
    @WithUserDetails(value = "jilee@example.com", setupBefore = TestExecutionEvent.TEST_EXECUTION)
    public void modifyAccount() throws Exception {
        AccountDto accountDto = new AccountDto();
        accountDto.setName("jilee");
        accountDto.setPassword("jilee321");
        String accountDtoJson = objectMapper.writeValueAsString(accountDto);

        mvc.perform(post("/account/update")
                .contentType(MediaType.APPLICATION_JSON)
                .content(accountDtoJson))
                .andDo(print())
                .andExpect(status().is3xxRedirection());
    }

}

@Transactional

@Transactional을 붙였다. Spring test에서 @Transactional을 붙이면 자동으로 트랜젝션을 롤백해줬었던걸 까먹고 @AfterEach로 데이터 처리하다가 갑자기 기억나서 @AfterEach는 지우고 @Transactional로 자동으로 롤백 되도록 바꾸었다.

@BeforeEach

@BeforeAll을 쓰다가 @BeforeEach 로 바꾸었다. @BeforeAll 이랑 @TestInstance(Lifecycle.PER_CLASS) 를 같이 사용하고 있었는데, (PER_CLASS가 아니면 BeforeAll 메소드가 static 메소드여야 한다...) 이렇게 테스트 하자니 테스트 메소드끼리 서로 영향을 주는 문제가 있어서 왠지 좋은 테스트 코드가 아니다 싶어서 @BeforeEach로 바꾸었다.

@WithUserDetails(value = "jilee@example.com", setupBefore = TestExecutionEvent.TEST_EXECUTION)

오늘의 하이라이트. 요놈 때문에 몇시간 고생한지 모르겠다.

이 프로젝트는 UserAccount 라는 User implements UserDetails 와 내가 계정 Entity로 사용하는 Account의 가운데 연결 역할을 하는 클래스와 커스텀 UserDetailsService 를 사용하고 있다. 이유는 email 주소를 아이디로 사용하려고!

public class UserAccount extends User {

    private Account account;

    public Account getAccount() {
        return account;
    }

    public UserAccount(Account account) {
        super(account.getEmail(),
                account.getPassword(),
                List.of(new SimpleGrantedAuthority("ROLE_"+account.getRole())));
        this.account = account;
    }

    public Long getAccountId() {
        return account.getId();
    }
}
@Service
public class AccountService implements UserDetailsService {

...

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Account account = accountRepository.findByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException(email));

        return new UserAccount(account);
    }
...
}

문제는 @WithMockUser 애노테이션으로 테스트하면 이 커스텀 UserDetailsService를 사용할 수 없다는것...

그래서 @WithUserDetails를 사용하게 되었는데, 또 @WithUserDetails는 @BeforeEach 메소드가 실행되기 전에 SecurityContext를 생성하고 UserDetails를 참조하려고 하기 때문에 @BeforeEach 메소드에서 만드는 계정을 사용할 수 없다는 것..

@BeforeEach 대신 @PostConstruct를 사용할 수 있지만 실패

@BeforeAll 을 대신 사용할 수 있지만 @BeforeEach를 사용하고 싶으므로 패스

결국 setupBefore = TestExecutionEvent.TEST_EXECUTION 이라는 옵션을 사용하면 @BeforeEach 메소드 후에 UserDetails를 참조하게 된다는걸 한참 지나서 알게됨. 근데 이 옵션도 버그가 있다는 말이 종종있다 ㄷㄷ.... 암튼 난 잘됨.

ObjectMapper

AccountDto 객체를 Json String 으로 바꾸기 위해 ObjectMapper DI


컨트롤러 리팩토링

    @PostMapping("/account")
    public String register(@RequestBody AccountDto accountDto) {
        accountService.registerAccount(accountDto);
        return "redirect:/login";
    }

    @PostMapping("/account/update")
    public String modify(@RequestBody AccountDto accountDto,
                         @AuthenticationPrincipal UserAccount userAccount) {
        accountService.modifyAccount(userAccount, accountDto);
        return "redirect:/login";
    }

요청 파라미터보다 요청 본문에 Dto 정보를 담고 싶어서 @ModelAttribute 에서 @RequestBody 로 변경.

글로벌 컨트롤러

@ControllerAdvice
public class GlobalController {

    @ExceptionHandler
    public ResponseEntity<?> UsedEmailExceptionHandler(UsedEmailException exception) {
        return ResponseEntity.badRequest().body("can't create account, because of used email.");
    }

    @ExceptionHandler
    public ResponseEntity<?> InvalidPasswordExceptionHandler(InvalidPasswordException exception) {
        return ResponseEntity.badRequest().body("can't create account, because of password.");
    }

    @ExceptionHandler
    public ResponseEntity<?> PermissionDeniedExceptionHandler(PermissionDeniedException exception) {
        return ResponseEntity.badRequest().body("permission denied.");
    }
}

일단은 테스트의 예외 처리를 위해 @ExceptionHandler 작성 수정이 필요할 듯 하다.

오늘은 요기까디~

Comments