인수 테스트코드에 대한 고찰

PPakSSam·2022년 1월 26일
0
post-thumbnail

1. 검증을 어디까지 해야할까?


@DisplayName("지하철 노선 목록 조회")
@Test
void getLines() {

// given
지하철노선_생성요청("2호선", "bg-green");
지하철노선_생성요청("3호선", "bg-orange");

// when
ExtractableResponse<Response> response = RestAssured
        .given().log().all()
        .when().get("/lines")
        .then().log().all().extract();

// then
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value());
List<String> lineNames = response.jsonPath().getList("name");
List<Integer> lineIds = response.jsonPath().getList("id");   

assertThat(lineIds).contains(1, 2);
assertThat(lineNames).contains("2호선", "3호선");

위의 테스트 코드에서 나는 지하철 노선 목록 조회니까 적어도 이름까지는 검증해야하지 않을까? 라고 생각하며 위의 코드를 짰다.

우선 pk값, 즉 id 값을 하나씩 하나씩 검증하는 것도 물론 의미가 있지만 한편으로는 그냥 값이 빈값이 아니기만 해도 되지 않을까? 라고 생각해볼 수 도 있을 것 같아요. 물론 내가 인자로 넘긴 값(ex.이름)이 제대로 처리되어서 응답오는지는 중요한 요소일 수 있어요. 이번 과정에서 수도없이 인수 테스트를 만들어 가실 예정인데 이 부분에 대해서도 경험해보시면서 자신만의 기준을 찾아보시는걸 추천드립니다.

리뷰어님의 답변인데 느낀 점은 정답은 없다라는 것이다.
정말 이름이 꼭 나와야하는 중요한 부분이면 이름을 검증할 것이고 아니면 검증을 하지 않아도 되므로 이는 상황마다 다를 것 같았다.
그리고 위의 경우는 아이디는 안 보여줘도 되므로 검증 안하고, 이름은 보여줘야 하므로 검증할 것 같다가 내 판단이다. 물론 생각은 언제든 바뀔 수 있으니까~~~ 라고 할뻔

 /**
     * Given 지하철 노선 생성을 요청 하고
     * When 새로운 구간의 생성을 요청하면
     * Then 새로운 구간 생성이 성공한다.
     */
    @DisplayName("지하철 노선의 구간 생성")
    @Test
    void createSection() {
        // given
        long sectionDownStationId = 지하철역_생성요청("영등포구청역")
                                    .jsonPath().getLong("id");
        int distance = 3;

        Map<String, String> params = new HashMap<>();
        params.put("upStationId", String.valueOf(firstDownStationId));
        params.put("downStationId", String.valueOf(sectionDownStationId));
        params.put("distance", String.valueOf(distance));

        // when
        ExtractableResponse<Response> response = RestAssured
                .given().log().all()
                .body(params)
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .when().post("/lines/" + lineId + "/sections")
                .then().log().all().extract();

        ExtractableResponse<Response> findLineResponse = 지하철노선_단건조회(lineId);

        // then
        assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value());       
    }

위의 코드는 지하철 노선(2호선)을 생성한 뒤 새로운 구간을 추가하는 것을 검증하는 코드이다.
처음 노선을 생성하면 하나의 구간(신도림역, 문래역)만 존재를 한다.
그런데 새로운 구간을 생성하면 첫번째 구간(신도림역, 문래역)과 두번째 구간(문래역, 영등포구청역)이 합쳐져서 2호선은 신도림역 - 문래역 - 영등포 구청역이 된다.

지하철역 생성의 경우 그냥 정보 입력하고 save만 하면 되니깐 그냥 상태코드가 201(CREATED)이면 되겠지 하면서 상태코드값만 검증을 했었다.
그런데 구간 생성의 경우 단순히 save만 하는 것이 아니고 노선에 구간을 추가하는 것이므로 단순히 지하철 역을 생성하는 것과는 다른 경우인 것이다.

인수 테스트의 검증 부분에서 실제로 구간 등록이 성공했는지를 검증하려면 어떻게 확인해볼 수 있을까요? 응답 코드로 충분하다고 판단하셨을 수 있지만 한번 고민해보는 것도 좋을 것 같아요!

그래서 위와 같은 리뷰를 받았고 아 ~ 그럼 새로 구간이 생성되면 2호선의 역이 3개가 되는 것이니까 그걸 검증하면 충분하겠다는 생각이 들었다.

assertThat(findLineResponse.jsonPath().getList("stations").size()).isEqualTo(3);

따라서 위와 같은 검증 로직 하나를 더 추가해서 안전한 테스트 코드로 진화(?) 시켰다.

마지막으로 리뷰어님의 답변으로 이 부분은 마무리하려고 한다.

인수 테스트의 검증 부분에서 유의할 점은 검증할 필요가 있는 것을 검증하는 것덜 깨지기 쉬운 테스트를 만들려면 어떻게 해야할까? 라는 고민일 것 같습니다.


2. 테스트 코드에서의 Utils


테스트 코드를 작성하다보면 특히 Given 부분에서 중복코드가 많이 발생한다.
이러한 중복 코드를 막기 위해 Utils가 필요하다고 하셨고 이에 대해 적용을 해봤다.

지하철 노선을 생성하려면 최소 2개의 지하철역이 필요하다.
따라서 Given에 다음과 같은 코드가 필요하다.

// Given
Map<String, String> params = new HashMap<>();
params.put("name", "신도림역");

ExtractableResponse<Response> response = RestAssured.given().log().all()
    .body(params)
    .contentType(MediaType.APPLICATION_JSON_VALUE)
    .when()
    .post("/stations")
    .then().log().all()
    .extract();

Map<String, String> params = new HashMap<>();
params.put("name", "문래역");

ExtractableResponse<Response> response2 = RestAssured.given().log().all()
    .body(params)
    .contentType(MediaType.APPLICATION_JSON_VALUE)
    .when()
    .post("/stations")
    .then().log().all()
    .extract();

딱봐도 너무 길고 노선을 생성할 때마다 역을 생성해야 하니 계속 위의 코드를 중복해서 사용을 해야 한다. 이 때 사용하는 것이 Utils이다.

public class StationUtils {

    public static ExtractableResponse<Response> 지하철역_생성요청(String name) {
        Map<String, String> params = new HashMap<>();
        params.put("name", name);

        return RestAssured.given().log().all()
                .body(params)
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .when()
                .post("/stations")
                .then().log().all()
                .extract();
    }
}

아까 그 Given의 길~~었던 코드가 위의 Utils 코드로 인해 얼마나 중복이 제거되었는지 보자

// Given
long upStationId = 지하철역_생성요청("신도림").jsonPath().getLong("id");
long downStationId = 지하철역_생성요청("문래").jsonPath().getLong("id");

보.이.는.가 😁😁😁 단 두줄로 처리할 수 있게 되었다.
덤으로 한글 메서드를 사용함으로써 가독성도 올라가게 되었다.

그런데 말입니다~ 고작 이정도로 만족하면 안되지 말입니다 ~

!? 이게 무슨말인가 😱😱 더 줄일게 있단 말인가??

요청을 보낼 때 RestAssured 관련 코드들이 중복되는 거 같습니다. 이를 위한 Utils를 만들어보면 어떨까요?

하하... 그렇구나 중복된 코드를 계속 작성하고 있으면 이를 인지를 하고 있어야 하는데
인텔리제이의 강력한 기능인 live template 때문에 전혀 인지하지 못하고 있었다
무슨 말이냐면 테스트 코드에서 요청을 보낼때 다음과 같은류의 코드를 작성한다.

ExtractableResponse<Response> response = RestAssured
                .given().log().all()
                .body(params)
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .when().post("/lines")
                .then().log().all().extract();

이런 류의 코드를 테스트 코드를 작성할 때마다 매번 작성하고 있었던 것이다...
그래서 다음과 같이 Utils를 작성해봤다.

public class RestAssuredUtils {

    public static ExtractableResponse<Response> get요청(String url) {
    
        return RestAssured
                .given().log().all()
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .when().get(url)
                .then().log().all().extract();
    }
    
    public static ExtractableResponse<Response> post요청(String url, Map<String,String> params) {
    
        return RestAssured
                .given().log().all()
                .body(params)
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .when().post(url)
                .then().log().all().extract();
    }
    
    ...

}

이러한 Utils를 사용하면 다음과 같이 중복코드도 없고 가독성도 좋은 테스트 코드가 만들어진다.

/**
* Given 상행 종점역 생성을 요청하고
* Given 하행 종점역 생성을 요청하고
* When 지하철 노선 생성을 요청 하면
* Then 지하철 노선 및 구간 생성이 성공한다.
*/
@DisplayName("지하철 노선 생성")
@Test
void createLine() {
  // given
  long upStationId = 지하철역_생성요청("신도림").jsonPath().getLong("id");
  long downStationId = 지하철역_생성요청("문래").jsonPath().getLong("id");

  Map<String, String> params = new HashMap<>();
  params.put("name", "2호선");
  params.put("color", "bg-green");
  params.put("upStationId", String.valueOf(upStationId));
  params.put("downStationId", String.valueOf(downStationId));
  params.put("distance", "7");

  // when
  post요청("/lines", params);

  // then
  assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value());
}

그런데 말입니다~ 이정도로도 아직 만족하면 안되지 말입니다 ~

그만!!!!! 거 장난이 너무심한거 아니오!!! 더 줄일게 있다고?????
그런데 나는 봐버렸다... 다음과 같은 리뷰를 말이다.

assert하는 부분도 Utils에 분리해보면 어떨까요?
ex) 생성_요청_실패(response) 생성_요청_성공(response)
이러면 가독성이 올라가면서 중복코드도 없앨 거 같아요.

하핫.... 리뷰를 받았으니 바꿔봐야지 😅😅😅

public class AssertUtils {

    public static 생성_요청_성공(ExtractableResponse<Response> response) {
        assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value())
    }
    
     public static 생성_요청_실패(ExtractableResponse<Response> response) {
    	assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value())
    }
}

여기까지 와보니 정말 리팩토링은 끝이 없구나를 느꼈다.
이렇게 다양한 리뷰를 받아보니 생각도 못했던 곳에서 리팩토링할 점을 발견할 수 있었다.
예전에 자주 즐겨보던 드림코딩 엘리의 영상에서 코드리뷰를 받기 전과 받은 후의 성장 차이는 정말 엄청난 차이가 있었다는 말을 들었었다. 몸소 느끼는 중이다 👍👍👍

3. 테스트 코드에서 중요한 점


테스트의 정밀성과 깨지기 쉬운 정도는 비례한다고 생각합니다.
테스트가 디테일하게 검증하는 경우 그 만큼 변경에 취약한 테스트가 될 것이고
변경에 영향을 최소로 받는 테스트의 경우 그 만큼 검증하는 대상을 꼼꼼히 검증하지 못할 것입니다.

강의 시간에 말씀드렸던 깨지기 쉬운 테스트의 이야기는 구현에 의존하는 테스트를 이야기하면서 말씀드린 부분이긴 한데 구현에 의존하는 테스트의 경우 요구사항은 그대로지만 리팩터링 하는 과정에서 테스트가 깨질 수 있으니 구현에 의존하기 보다는 최대한 덜 영향을 받는 블랙박스 테스트를 말씀드린것입니다.

3단계 요구사항의 경우 요구사항 자체가 변경되면서 테스트가 검증하는 대상의 결과값이 변경되는 경우이기 때문에 테스트가 변경되는 것을 피할 순 없을 것 같습니다. 테스트하는 대상이 변경되기 때문이죠. 우리는 이번 과정을 통해서 깨지지 않는 테스트를 추구하기 보다는 덜 깨지기 쉬운 테스트를 추구해보면 좋을 것 같습니다.

확실히 RestAssured를 이용하는 테스트는 블랙박스 형식의 테스트로 구현이 바뀌더라도 테스트가 깨지지는 않는다.

그런데 만약 지하철 노선을 생성하는데 필요한 인자가 원래 2개였는데 5개로 늘어났다면?
이런 경우에는 테스트는 필연적으로 깨질 수 밖에 없다. 왜냐하면 원래 인자 2개로 노선을 생성해왔고 그걸 테스트 했을 테니까!

이런 경우 좀 더 유연하게 대처할 수 있는 방법이 없을까? 고민을 해봤다.

앞서 설명한 Utils 클래스의 노선을 생성하는 메소드에서 원래 2개의 인자만 필요했는데 5개의 인자가 필요해졌다고 가정하자.

// 원래 메소드
public static ExtractableResponse<Response> 
                  지하철노선_생성요청(String name, String color) 
                  {...}

// 바뀐 메소드
public static ExtractableResponse<Response> 
                  지하철노선_생성요청(String name, String color, Long upStationId,
                                   Long downStationId, int distance) 
                  {...}                  

위와 같이 바꿨다고 가정하자.
만약 인자가 8개로 늘어나면?? 계속해서 파라미터는 늘어날 것이고 바뀌는 부분이 많아지게 되며,
가독성도 안좋게 된다.

빌더 패턴을 한번 사용해보자!!

지하철노선_생성요청에 들어갈 파라미터로 LineRequest 클래스를 만들고, 빌더 패턴을 적용하는 것을 시도해 봤다.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LineRequest {
    private String name;
    private String color;
    private Long upStationId;
    private Long downStationId;
    private int distance;

    @Builder
    private LineRequest(String name, String color, Long upStationId, 
    			Long downStationId, int distance) {
        this.name = name;
        this.color = color;
        this.upStationId = upStationId;
        this.downStationId = downStationId;
        this.distance = distance;
    }
}
public static ExtractableResponse<Response> 
                  지하철노선_생성요청(LineRequest request) {...}

// 사용 부분
LineRequest request = LineRequest.builder()
        .name("2호선")
        .color("bg-green")
        .upStationId(upStationId)
        .downStationId(firstDownStationId)
        .distance(7)
        .build();

lineId = 지하철노선_생성요청(request).jsonPath().getLong("id");

이렇게 코드를 짜면 앞으로 필요한 인자가 추가되더라도 유연하게 대처할 수 있으며,
가독성도 더 좋아지게 된다.

이 부분은 스스로 고민하면서 시도한 부분인데 리뷰어님의 칭찬을 받으니 기부니가 아주 좋아졌었다 ㅋㅋㅋ 🖐🖐🖐

profile
성장에 대한 경험을 공유하고픈 자발적 경험주의자

0개의 댓글