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 사용 방법
- 가짜 객체를 만들어서 injection 될 @Mock 객체와 injection 당할 @InjectMock를 지정함
- 테스트 케이스마다 mock 객체로 실행할 코드와 결과를 지정함 혹은 mock 객체 메소드 실행 후 실행되었는지만 체크함 : when ~ thenXXX API, verify()
- 테스트 비교 : 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 예를 들어서(어떻게 적용되는지)