본문 바로가기
Education/우아한테크캠프Pro

[우아한테크캠프Pro] 4주차 - ATDD

by 잭피 2021. 1. 26.

 

안녕하세요~ 잭코딩입니다!

 

(해당 업체에서 광고를 받지 않았으며, 교육비를 직접 내고 교육을 받고 있습니다)

교육을 받으며 느낀 점을 적기 위한 포스팅입니다


아주 오랜만에 후기를 작성하네요

 

미션이 뒤로갈수록 어렵고 시간이 오래걸리네요

또 요즘 면접을 준비하느라 블로그를 작성할 시간이 없었습니다..

최근 최종합격하여 이직하게 되었고, 다시 미션을 집중해서 하고 있습니다

 

1. 초간단 자동차 경주 게임 

2. 로또 - TDD

3. 지하철 노선도 - JPA

4. 지하철 노선도 - ATDD

5. 지하철 노선도 - ATDD 내에서 TDD

6. 지하철 노선도 - 레거시 코드 리팩토링 

 

마지막 미션 총 4개의 스탭 중 2번째 진행중인데 마감기한은 1월 31일 낮 12시까지네요...

잘 끝내서 꼭 수료해보겠습니다!

 

그럼 기억을 더듬어 4주차 지하철 노선도 - ATDD 미션 후기를 남겨보겠습니다

 

ATDD

ATDD란 Acceptance Test Driven Development로 인수테스트 주도 개발이라는 뜻입니다

원래는 다양한 관점을 가진 팀원들과 협업을 위한 애자일 방법 중 하나라고 합니다

 

요구사항 -> 인수 테스트 -> 문서화 -> TDD -> API 개발 -> 테스트 리팩터링을 통해 ATDD 개발 프로세스를 진행할 수 있습니다

 

인수 테스트(Acceptance Test)란 사용자의 관점에서 올바르게 작동하는지 테스트입니다

이런 인수 조건은 개발자가 아닌 일반 사용자들이 이해할 수 있는 단어를 사용하여 만듭니다

 

실무에서 기획자가 전달했던 요구사항이 충족되었는지를 확인하는 테스트라고 생각할 수도 있습니다

 

이런 인수 테스트는 전체 시스템을 대상으로 세부 구현 영향을 받지 않도록 구현해야합니다 

 

한번 예시를 통해 보겠습니다

한 기획자분이 개발자분과 요구사항을 아래와 같이 정리하였습니다

 

지하철 노선 관련 기능
1. 지하철 노선을 생성하는 기능
2. 지하철 노선 전체를 조회하는 기능
3. 지하철 노선을 조회하는 기능
4. 지하철 노선을 수정하는 기능
5. 지하철 노선을 제거하는 기능

 

이러한 요구사항을 기반으로 개발자는 인수 테스트를 작성할 수 있습니다

@DisplayName("지하철 노선 관련 기능")
public class LineAcceptanceTest extends AcceptanceTest {


    @DisplayName("지하철 노선을 생성한다.")
    @Test
    void createLine() {
        // given
        지하철_역_생성_요청("강남역");
        지하철_역_생성_요청("역삼역");

        // when
        ExtractableResponse<Response> response = 지하철_노선_생성_요청("신분당선", "bg-red-600", "1","2","10");

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

    @DisplayName("기존에 존재하는 지하철 노선 이름으로 지하철 노선을 생성한다.")
    @Test
    void createLine2() {
        // given
        지하철_역_생성_요청("강남역");
        지하철_역_생성_요청("역삼역");
        지하철_노선_생성_요청("신분당선", "bg-red-600", "1","2","10");

        // when
        // 지하철_노선_생성_요청
        ExtractableResponse<Response> response = 지하철_노선_생성_요청("신분당선", "bg-red-600", "1","2","10");

        // then
        // 지하철_노선_생성_실패됨
        Assertions.assertThat(response.statusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value());
    }

    @DisplayName("지하철 노선 목록을 조회한다.")
    @Test
    void getLines() {
        // given
        지하철_역_생성_요청("강남역");
        지하철_역_생성_요청("역삼역");
        지하철_노선_생성_요청("2호선", "green", "1","2","10");

        // when
        ExtractableResponse<Response> response = 지하철_노선_목록_조회_요청();

        // then
        // 지하철_노선_목록_응답됨
        Assertions.assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value());

        // 지하철_노선_목록_포함됨
        List<String> lineNames = response.jsonPath().getList(".", LineResponse.class).stream()
                .map(lineResponse -> lineResponse.getName())
                .collect(Collectors.toList());
        Assertions.assertThat(lineNames).contains("2호선");
    }

    @DisplayName("지하철 노선을 조회한다.")
    @Test
    void getLine() {
        // given
        지하철_역_생성_요청("강남역");
        지하철_역_생성_요청("역삼역");
        ExtractableResponse<Response> request = 지하철_노선_생성_요청("신분당선", "bg-red-600", "1","2","10");

        // when
        // 지하철_노선_조회_요청
        LineResponse lineResponse = request.jsonPath().getObject(".", LineResponse.class);
        ExtractableResponse<Response> response = 지하철_노선_조회_요청(lineResponse.getId());

        // then
        // 지하철_노선_응답됨
        Assertions.assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value());
    }

    @DisplayName("존재하지 않는 지하철 노선을 조회한다.")
    @Test
    void lineNotFoundException() {
        // when
        // 지하철_노선_조회_요청
        ExtractableResponse<Response> response = 지하철_노선_조회_요청(1l);

        // then
        // 지하철_노선_응답됨
        Assertions.assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value());
    }

    @DisplayName("지하철 노선을 수정한다.")
    @Test
    void updateLine() {
        // given
        지하철_역_생성_요청("강남역");
        지하철_역_생성_요청("역삼역");
        ExtractableResponse<Response> request = 지하철_노선_생성_요청("4호선", "blue","1","2","10");

        // when
        // 지하철_노선_수정_요청
        LineResponse lineResponse = request.jsonPath().getObject(".", LineResponse.class);
        ExtractableResponse<Response> response = 지하철_노선_수정_요청(lineResponse.getId(), "2호선", "green");

        // then
        // 지하철_노선_수정됨
        LineResponse finalResponse = response.jsonPath().getObject(".", LineResponse.class);
        Assertions.assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value());
        Assertions.assertThat(finalResponse.getName()).isEqualTo("2호선");
        Assertions.assertThat(finalResponse.getColor()).isEqualTo("green");
    }

    @DisplayName("지하철 노선을 제거한다.")
    @Test
    void deleteLine() {
        // given
        지하철_역_생성_요청("강남역");
        지하철_역_생성_요청("역삼역");
        ExtractableResponse<Response> request = 지하철_노선_생성_요청("4호선", "blue", "1","2","10");

        // when
        // 지하철_노선_제거_요청
        LineResponse lineResponse = request.jsonPath().getObject(".", LineResponse.class);
        ExtractableResponse<Response> response = 지하철_노선_삭제_요청(lineResponse.getId());

        // then
        // 지하철_노선_삭제됨
        Assertions.assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value());
    }

이렇게 기획자가 요구한 요구사항을 충족하였는가를 테스트 코드로 작성합니다

 

여기서 지하철 노선 생성 요청, 응답 등은 RestAssured를 사용했습니다

testImplementation 'io.rest-assured:rest-assured:3.3.0'
public class LineRestAssuredUtils {

    public static ExtractableResponse<Response> 지하철_노선_생성_요청(String name, String color, String upStationId, String downStationId, String distance) {
        Map<String, String> params = getLineParams(name, color, upStationId, downStationId, distance);

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

    public static ExtractableResponse<Response> 지하철_노선_목록_조회_요청() {
        return RestAssured.given().log().all()
                .when()
                .get("/lines")
                .then().log().all()
                .extract();
    }

    public static ExtractableResponse<Response> 지하철_노선_조회_요청(Long lineId) {
        return RestAssured.given().log().all()
                .when()
                .get("/lines/" + lineId)
                .then().log().all()
                .extract();
    }
  }

자주 쓰이는 생성, 조회 요청은 하나로 모아서 관리하도록 하였습니다

 

이렇게 작성한 인수 테스트 코드는 당연히 처음에 실패합니다

(아직 서비스 및 도메인 등 코드가 없고 지하철 관련 비지니스 로직이 존재하지 않습니다)

 

이러한 요청과 응답을 기반으로 요구사항의 도메인 로직을 TDD를 통해 작성해나가면 보다 안전하게 개발할 수 있었습니다

 

물론 여러 예외상황도 발생할 수 있습니다

예를 들어, 잘못 요청한 경우 400 에러를 보내줘야 하는 경우가 있습니다

이러한 예외상황에 대한 처리는 통합 테스트나 단위 테스트에서 처리하는게 보다 좋다고 생각합니다

 

인수 테스트는 이 서비스가 정상적인 사용자 관점에서 잘 작동하는지에 대한 최소한의 테스트를 작성하는게 좋다고 생각합니다

 

이번 미션에서는 이렇게 먼저 요구사항을 작성하고 이를 기반으로 인수 테스트를 작성하였습니다

그리고 TDD를 통해 서비스, 도메인을 개발하는 연습을 할 수 있었습니다

 

다음 미션에서는 이러한 인수 테스트를 통합하는 연습을 합니다
보다 효율적으로 관리할 수 있도록 시나리오를 통해 인수 테스트를 작성합니다

 

글쓴이는 다음 미션에서 배운 시나리오 기반 인수 테스트를 실제 실무에도 적용해보고 있습니다
고객의 정상적인 요청과 응답을 몇 가지 시나리오로 만듭니다
빌드할 때 이 시나리오 기반 인수 테스트들이 모두 성공한다면 운영에 올라가도 큰 이슈는 없다고 판단할 수 있습니다

 

다음주에 인수 테스트 통합 미션 리뷰를 하면서 실제 어떻게 적용하고 있는지 간단히 적어보겠습니다

다음 5주차 수업과 미션은 인수 테스트 통합과 관련된 내용입니다

 

이번주에 모든 미션 완수하고 리뷰 남기러 오겠습니다!

 

댓글