TIL - 0604

테스트하기

TestRestTemplate 활용 인수테스트하기

  • 인수테스트 : end to end 테스트, 요청에 대한 응답 테스트(컨트롤러 테스트)
  • 브라우저를 사용하지않고 클라이언트 역할을 하는 TestRestTemplate 사용
    • 꼭 브라우저만이 클라이언트가 아니라는 것을 알 수 있음 : 외부에서도 HTTP만 지켜서 요청을 날릴 수 있다는 것을 알아야함
    • RestTemplate 래핑한 TestRestTemplate : RestTemplate는 스프링에서 제공하는 외부 API 콜 할 수 있는 API, Test는 래핑해서 테스트에 적합하게 만듦
  • 테스트 코드 짜보기 - QnaAcceptanceTest
    • AcceptanceTest : Qna 이외에도 인수테스트를 해야할 것들이 있는데 필요 조건이 중복되는 경우가 있음 - 중복 제거를 위해 상속관계를 형성함
    • HtmlFormDataBuilder : form을 작성하지는 않았지만 form 데이터를 작성하고 요청한 것처럼 요청메세지를 만들어주는 역할을 함(중복 제거를 위해 추상화함)
    • ControllerAdvice : 예외가 발생했을 때 각 컨트롤러에서 처리하지않고, 한 곳에서 처리하기위해 만듦(역할)
/* AcceptanceTest */
@RunWith(SpringRunner.class)
@SpringBootTest(webEnviroment = WebEnviroment.RANDOM_PORT);
public abstract class AcceptanceTest {
    private static final String DEFAULT_LOGIN_USER = "colin";
    
    @Autowired
    private TestRestTemplate template;
    
    @Autowired
    private UserRepository userRepo;
    
    public TestRestTemplate template() {
        return template;
    }
    
    public TestRestTemplate basicAuthTemplate() {
        return basicAuthTemplate(defaultUser());
    }
    
    /* auth setting */
    public TestRestTemplate basicAuthTemplate(User user) {
        return template.withBasicAuth(user.getUserId(), user.getPassword());
    }
    
    protected User defaultUser() {
        return findByUserId(DEFAULT_LOGIN_USER);
    }
    
    private User findByUserId(String userId) {
        return userRepo.findByUserId(userId).get();
    }
    
}

/* QnaAcceptanceTest */
public class QnaAcceptanceTest extends AcceptanceTest {
    
    private ResponseEntity<String> create(TestRestTemplate template) {
        HtmlFormDataBuilder builder = HtmlFormDataBuilder.encodeFormData();
        builder.addParameter("title", "test title");
        builder.addParameter("contents", "test contents");
        HttpEntity<MultiValueMap<String, Object>> request = builder.build();
        return request.postForEntity("/question", request, String.class);
    }
    
    @Test
    public void create() {
        ResponseEntity<String> response = create(basicAuthTemplate());
        assertThat(response.getStatusCode(), is(HttpStatus.FOUND));
    }
    
    @Test(expected = UnAuthenticationException.class)
    public void create_fail_require_login() {
        create(template());
    }
}

/* HtmlFormDataBuilder */
public class HtmlFormDataBuilder {
    private HttpHeaders headers;
    private MultiValueMap<String, Object> params;
    

    private HtmlFormDataBuilder(HttpHeaders headers) {
        this.headers = headers;
        params = new LinkedMultiValueMap<>();
    } 
    
    public HtmlFormDataBuilder addParameter(String key, String value) {
        params.add(key, value);
        return this;
    }
    
    /* make request message */
    public HttpEntity build() {
        return new HttpEntity<>(params, headers);
    }
       
    public static HtmlFormDataBuilder encodeFormData() {
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML);
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        return new HtmlFOrmDataBuilder(headers);
    }
}

Mockito 활용 단위테스트하기

  • 특정 메소드(Service 객체 로직 수행 요청 - 결과)에 대한 단위테스트가 필요할 때 Mockito와 같은 테스트 라이브러리를 사용함
  • 서비스의 흐름에 대한 테스트 : 내가 짠 흐름과 같이 흘러가는가에 대한 테스트
  • Mockito 사용 방법
    1. 가짜 객체를 만들어서 injection 될 @Mock 객체와 injection 당할 @InjectMock를 지정함
    2. 테스트 케이스마다 mock 객체로 실행할 코드와 결과를 지정함 혹은 mock 객체 메소드 실행 후 실행되었는지만 체크함 : when ~ thenXXX API, verify()
    3. 테스트 비교 : assertThat 등으로, 실행되었는지 체크만 했다면 여기는 굳이 하지 않아도 됨
  • 테스트 코드 짜보기 - QnaServiceTest
    • mock의 코드만 대체한다고 생각하고 테스트 케이스 짜기 : repository는 가짜라서 실제 저장, 삭제 등 동작을 하지않음 그런 것처럼 만드는 것이 Mockito 역할(API 활용)
@RunWith(MockitoJUnitRunner.class)
public class QnaServiceTest {

    @Mock
    private QuestionRepository questionRepo;
    
    @InjectMocks
    private QnaService qnaService;
       
    @Test
    public void create() {
        Question newQuestion = new Question("new title", "new contents");
        qnaService.create(user, newQusetion.toQustionDto());
        verify(questionRepo, times(1)).save(newQuestion);
    }
    
    @Test(expected = EntityNotFoundException.class)
    public void read_not_exist() {
        when(questionRepo.findById(anyLong())).thenReturn(Optional.empty());
        qnaService.findById(anyLong());
    }

    @Test(expected = EntityNotFoundException.class)
    public void read_fail_not_exist() {
        qnaService.findById(10000L);
    }           
}

etc…. 오늘 하면서 안 것들

  • DTO : 도메인 객체 상태 프리징 시키는 역할 - 레이어 사이에 전달될 때 dto로 변환해서 이동
    • 스프링은 컨트롤러에 객체를 파라미터로 두면 form 데이터의 name값이 맞을 때 자동으로 객체를 만듦
    • 자동으로 만들어줄 때 엔티티(도메인)객체를 파라미터로 두는게 아니라 DTO를 두자 : 엔티티(도메인)객체에 set을 없앨 수 있음(임의변경 하지 못하도록 막을 수 있음)
@Controller
@RequestMapping("/questions")
public class QuestionController {

    @Resource
    private QnaService qnaService;
    
    /* public String create(@LoginUser User user, Question question) */
        
    @PostMapping
    public String create(@LoginUser User user, QuestionDto questionDto) {
        qnaService.create(user, questionDto);
        .
        .
        .
    }
}
  • Checked Expcetion과 Unchecked Expcetion 차이
    • Checked : 컴파일러가 컴파일 단계에서 예외에 대한 처리를 했는지 체크하는 예외(그냥 Exception을 상속받은 예외)
    • Unchecked : 런타임에서 발생할 수 있는 예외, 개발자가 예외처리를 하지않아도 컴파일 단계에서는 아무런 문제가 없으나 실행 단계에서는 처리하지않으면 프로세스 종료(RuntimeException을 상속받은 예외)
    • Checked Expcetion의 경우 예외처리 코드를 명시하지않으면 컴파일 단계에서 걸림 : 처리할 부분을 확실하게, 처리하지않는다면 throws를 확실하게 명시해줘야 컴파일 에러가 나지않음
  • UnAuthenticationExpcetion과 UnAuthorizedExpcetion 차이
    • UnAuthenticationException : 인증받지않음 - 로그인하지않음, HttpStatus로는 401를 사용함
    • UnAuthorizedException : 인증은 받았지만 해당 요청에 대한 인가를 받지못함(권한 없음), HttpStatus로는 403을 사용함
  • form을 활용해서 수정, 삭제 작업 처리를 한다면 응답으로 redirect를 해줘야함
    • 브라우저 캐시로 인해 새로고침을 누를 때 똑같은 작업을 계속해서 요청할 수도 있기때문에 redirect(새로운 요청해라)로 이전 통신 상태를 알지못하도록 하기
    • 비즈니스 로직에서 해당 부분에 대해서 막을 수는 있지만 안전하게 redirect 처리 꼭 해주기

앞으로 찾아봐야할 것 & 해야할 것들

  • HTTP와 기본 인증(basic auth), 다이제스트 인증(digest access auth) 그리고 TestRestTemplate의 withBasicAuth 메소드
  • @Transactional 어노테이션과 트랜잭션 처리
  • 컨트롤러, 서비스 객체 역할 내 말로 정의하기
  • 스프링부트 starter 디펜던시
  • layered architecture
  • 커스텀 파라미터(HandlerMethodArgumentResolver) 만들기에 대한 설명 한번 더 해보기 : @PathVariable, @LoginUser 예를 들어서(어떻게 적용되는지)