[목표]
- 인수테스트 기반 TDD을 도와주는
RestAssured
를 이해할 수 있다. - 언제 인수테스트 기반 TDD 를 사용하는 것이 적절한지, 이해할 수 있다.
- 왜 인수테스트 기반 TDD를 사용하는 것이 적절한지 이해할 수 있다.
- 어떻게 인수테스트 기반 TDD를 사용하는 것이 적절한지 이해할 수 있다.
인수테스트 기반 TDD을 도와주는 RestAssured
를 이해할 수 있다.
RestAssured
의 공식사이트
RestAssured
는 REST service 를 테스트할 때 도움을 주는 테스트 도구 이다. 우리가 흔히 사용하는 GET
, POST
, PUT
, DELETE
메소드를 지원한다.
만약, GET lotte/5
을 했을 경우 아래와 같은 경우가 나온다면?
아래 내용은 RestAssured 공식 사이트에서 가져온 것이다.
{
"lotto":{
"lottoId":5,
"winning-numbers":[2,45,34,23,7,5,3],
"winners":[
{
"winnerId":23,
"numbers":[2,45,34,23,3,5]
},
{
"winnerId":54,
"numbers":[52,3,12,11,18,22]
}
]
}
}
@Test public void
lotto_resource_returns_200_with_expected_id_and_winners() {
when().
get("/lotto/{id}", 5).
then().
statusCode(200).
body("lotto.lottoId", equalTo(5),
"lotto.winners.winnerId", hasItems(23, 54));
}
그 외 좀 더 자세한건 공식사이트를 통해서 학습해보자!
언제
인수테스트 기반 TDD를 사용할까?
1. 여러 도메인이 복잡하게 얽혀있어 분리해내기 어려울때
public void deleteStation(Long lineId, Long stationId) {
Station station = stationDao.findById(stationId);
Line persistLine = lineDao.findById(lineId);
List<Edge> oldEdges = persistLine.getEdges();
Edges edges = persistLine.getEdges();
List<Edge> replaceEdge = edges.stream()
.filter(it -> it.hasStation(station))
.collect(Collectors.toList());
...
Edge newEdge = updatedEdges.getEdges().stream()
.filter(it -> !oldEdges.contains(it))
.findFirst().orElseThrow(RuntimeException::new);
edgeDao.deleteByStationId(stationId);
edgeDao.save(persistLine.getId(), newEdge);
}
위와 같은 비지니스 로직이 있다고 가정하자.
Station
, Line
, Edges
등의 여러 도메인이 섞여있어 테스트하기 어려운 코드입니다. 그러한 코드일때는 어떻게 하면 좋을까?
이렇게 복잡하게 되어있다는 것은 설계상 도메인관계가 복잡하게 되어있다는 말과 유사하다. 이를 해결하기 위해서는 ATDD 를 활용할 수 있다.
새로운 방법으로 TDD를 접근
When we’re implementing a feature, we start by writing an acceptance test, which exercises the functionality we want to build. While it’s failing, an acceptance test demonstrates that the system does not yet implement that feature; when it passes, we’re done.
Growing object-oriented software, guided by tests
새로운 방법으로 TDD를 접근
우리는 기능을 구현할 때, 만들고자 하는 기능을 수행하는 인수 테스트를 작성하는 것으로 시작한다. 인수 테스트가 실패하는 동안은 시스템이 아직 그 기능을 구현하지 않고 있다는 것을 보여준다; 인수 테스트가 통과되면, 기능 구현은 끝이다.
- 테스트 주도 개발로 배우는 객체 지향 설계와 실천
2. 스프링 프레임워크에 의존하지 않는 테스트를 만들고 싶을 때
우리가 만드는 대부분의 테스트는 스프링 프레임워크에 의존합니다. 예를 들어 @WebMvcTest
, @DataJpaTest
, @ExtendWith(SpringExtension.class)
등은 스프링에서 제공되는 테스트 도구에 의해서 동작된다.
이러한 의존관계없이, 테스트 해볼 수 있다. 내가 느꼈던 유용한 부분은 Spring security
를 사용하는 경우, 인증이 필요한 메소드를 실행시킬 경우, 코드가 일부 부자연스러운 현상들이 있었다.(어노테이션을 통해서 억지로 Spring security
를 사용하지 않는다는 등) 이런 문제에 대해서 ATDD는 꽤 직관적이고 자연스럽다.
코드의 일부를 소개하면 다음과 같다.
만약 지하철 노선은 오직 인증된 사용자만 생성할 수 있다면?
@DisplayName("지하철 노선을 생성한다.")
@Test
void createLine() {
ExtractableResponse<Response> response = 토큰을_요청한다(request);
정상적으로_동작됨(response);
정상적으로_로그인됨(토큰으로_로그인_요청함(response.as(TokenResponse.class)));
// when
ExtractableResponse<Response> response = 지하철_노선_생성_요청(lineRequest1);
// then
지하철_노선_생성됨(response);
}
이러한 방식으로, 로그인을 먼저 시도한 뒤 노선을 생성한다. 주관적인 생각으로는 꽤 자연스럽다고 생각한다.
왜
인수테스트 기반 TDD 를 사용하면 좋을까?
인수테스트 기반 TDD를 사용하면 사용자 관점에서 테스트 코드를 이해할 수 있게 된다.
흔히 말하는 UserCase
에 대해서도 100% 까지 커버할 수 있다. 라는 관점보다는 구현과정에 있어 해피케이스
에 대한 도움을 적극 받을 수 있다.
마찬가지로 한가지 테스트 코드를 아래에 첨부한다.
@DisplayName("회원 정보를 관리한다.")
@Test
void manageMember() {
// when
ExtractableResponse<Response> createResponse = 회원_생성을_요청(EMAIL, PASSWORD, AGE);
// then
회원_생성됨(createResponse);
// when
ExtractableResponse<Response> findResponse = 회원_정보_조회_요청(createResponse);
// then
회원_정보_조회됨(findResponse, EMAIL, AGE);
// when
ExtractableResponse<Response> updateResponse = 회원_정보_수정_요청(createResponse, NEW_EMAIL, NEW_PASSWORD, NEW_AGE);
// then
회원_정보_수정됨(updateResponse);
// when
ExtractableResponse<Response> deleteResponse = 회원_삭제_요청(createResponse);
// then
회원_삭제됨(deleteResponse);
}
@DisplayName("나의 정보를 관리한다.")
@Test
void manageMyInfo() {
회원_생성됨(회원_생성을_요청(EMAIL, PASSWORD, AGE));
ExtractableResponse<Response> response = AuthAcceptanceTest.토큰을_요청한다(new TokenRequest(EMAIL, PASSWORD));
ExtractableResponse<Response> findResponse = 나의_정보_조회_요청(response.as(TokenResponse.class));
회원_정보_조회됨(findResponse, EMAIL, AGE);
ExtractableResponse<Response> updateResponse = 나의_정보_수정_요청(response.as(TokenResponse.class), NEW_EMAIL, NEW_PASSWORD, NEW_AGE);
회원_정보_수정됨(updateResponse);
ExtractableResponse<Response> refindResponse = 나의_정보_조회_요청(response.as(TokenResponse.class));
회원_정보_조회됨(refindResponse, NEW_EMAIL, NEW_AGE);
ExtractableResponse<Response> deleteResponse = 나의_정보_삭제_요청(response.as(TokenResponse.class));
회원_삭제됨(deleteResponse);
}
이러한 유저케이스를 커버하는 테스트 코드가 하나 존재한다면, 우린 Member
클래스에 대한 모든 행복케이스
에 대해서는 테스트를 완료한 것이다. 각각의 인수테스트를 만들고 통과시키면서 우리는 원했던 기능구현을 이어갈 수 있게 되었다.
어떻게
인수테스트 기반 TDD를 사용하는 것이 적절한지 이해할 수 있다.
그러면 마지막으로 어떻게
인수테스트를 작성하면 좋을까?
이미 위 메소드를 보면서 눈치챈 사람들도 있을 수 있다.
대부분 하나의 컨트롤러를 기반으로 만들지만, 한가지 명심해야 될 부분이 있다. 만약 GET /members/me
을 인수테스트로 한다면, 내가 무엇을 테스트 할 것인가에 대해서 명확히 의도를 노출할 수 있도록 메소드 명을 작성해야 한다. GET /members/me
할 경우, 나의_정보_조회_요청()
이라는 의도를 노출하는 이름으로 테스트명을 작성해야 한다.
private ExtractableResponse<Response> 나의_정보_조회_요청(TokenResponse tokenResponse) {
return RestAssured.given().log().all()
.auth().oauth2(tokenResponse.getAccessToken())
.contentType(MediaType.APPLICATION_JSON_VALUE)
.when().get("/members/me")
.then().log().all().extract();
}
결론
테스트하기 쉽고, 스프링에 의존하지 않는 테스트라는 관점에서 꽤 매력적인 도구이다. 그러나, 반대로 작성해야될 테스트 코드가 늘어다는 단점도 있다.
'가치관 쌓기 > 개발 돌아보기' 카테고리의 다른 글
직접 코딩으로 느껴본 Spring Data JPA와 Spring Data JDBC 의 차이점 (0) | 2021.07.18 |
---|---|
[우아한테크코스Pro] 서비스 진단하기 - 1 (Logging) [6/9] (0) | 2021.07.16 |
[우아한테크코스Pro] 인수 테스트 기반 TDD - 1 [5/9] (0) | 2021.07.01 |
[우아한테크코스Pro] 그럴듯한 서비스 만들기 - 2 [4/9] (0) | 2021.07.01 |
[우아한테크코스Pro] 그럴듯한 서비스 만들기(network) - 1 [4/9] (0) | 2021.06.17 |
댓글