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

[우아한테크캠프Pro] 5주차 - ATDD 통합

by 잭피 2021. 1. 27.

 

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

 

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

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


미션 코드리뷰 PR요청 후, 시간이 조금 생겨서 5주차 리뷰도 빠르게 남겨보겠습니다

 

5주차 지하철 노선도 - ATDD 내에서 TDD 미션 후기입니다

 

인수 테스트 통합

API를 검증하기 보다는 시나리오, 흐름을 검증하는 테스트로 통합하는 과정입니다

예를 들어 고객이 사이트에 들어와서 수행할 수 있는 과정을 시나리오로 작성할 수 있습니다

 

한번 저번 미션에서 작성한 통합 전 인수 테스트를 볼까요?

@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 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 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");
    }
}

이 테스트는 API 기능 단위로 검증을 하고있습니다

각 인수 테스트를 관리하는 부담도 크고 중복 코드도 많습니다

 

이러한 인수 테스트를 시나리오로 구성해서 하나의 인수 테스트로 통합할 수 있습니다

인수 테스트를 통합한다면 아래와 같이 시나리오를 구성할 수 있습니다

Feature: 지하철 구간 관련 기능
  Background
    Given
    지하철역 등록되어 있음
    And
    지하철 노선 등록되어 있음
    And
    지하철 노선에 지하철역 등록되어 있음
  Scenario
  : 지하철 구간을 관리
    When
    지하철 구간 등록 요청
    Then
    지하철 구간 등록됨
    When
    지하철 노선에 등록된 역 목록 조회 요청
    Then
    등록한 지하철 구간이 반영된 역 목록이 조회됨
    When
    지하철 구간 삭제 요청
    Then
    지하철 구간 삭제됨
    When
    지하철 노선에 등록된 역 목록 조회 요청
    Then
    삭제한 지하철 구간이 반영된 역 목록이 조회됨

그리고 이러한 시나리오에 맞게 인수 테스트를 작성합니다

실제 코드 변경으로 인해 운영에 배포할 때도 이 시나리오 테스트들이 든든한 역할을 합니다

일반적인 고객의 시나리오는 성공한다는 검증이 되기 때문입니다

 

5주차에서는 회원 정보, 지하철 최단 경로 조회, 권한 인증등 다양한 미션을 진행했습니다

이러한 미션을 진행하기 전에 아래처럼 시나리오를 작성하고 인수 테스트를 가장 먼저 작성하였습니다

* 토큰 발급 기능 (로그인) 인수 테스트 만들기
    * AuthAcceptanceTest
    ```
        Feature: 로그인 기능
            Scenario: 로그인을 시도한다.
                Given 회원 등록되어 있음
                When  로그인 요청
                Then  로그인 됨
  
                When  잘못된 정보로 로그인 요청
                Then  로그인 실패됨  
  
                When  토큰으로 나의 정보 조회를 요청
                Then  나의 정보가 조회됨
  
                When  잘못된 토큰으로 나의 정보 조회를 요청
                Then  나의 정보가 조회되지 않음 
                
  * 인증 - 내 정보 조회 기능 완성하기
    * MemberAcceptanceTest
        * /members/me URI 요청으로 동작을 검증 
        * 로그인 후 발급 받은 토큰을 포함해서 요청
        ```
            Feature: 회원 정보 관리 기능
                Scenario: 나의 정보를 관리한다
                    Given 로그인이 되어있음
                    When  나의 정보 조회를 요청
                    Then  나의 정보가 조회됨
    
                    Given 로그인이 되어있음
                    When  나의 정보 수정을 요청
                    Then  나의 정보가 수정됨        
                    
                    Given 로그인이 되어있음
                    When  나의 정보 삭제를 요청
                    Then  나의 정보가 삭제됨 
        ```
* 인증 - 즐겨 찾기 기능 완성하기 
    * 즐겨찾기 기능을 완성하기
    * 인증을 포함하여 전체 ATDD 사이클을 경험할 수 있도록 기능을 구현하기
        ```
            Feature: 즐겨찾기를 관리한다.
            
              Background 
                Given 지하철역 등록되어 있음
                And 지하철 노선 등록되어 있음
                And 지하철 노선에 지하철역 등록되어 있음
                And 회원 등록되어 있음
                And 로그인 되어있음
            
              Scenario: 즐겨찾기를 관리
                When 즐겨찾기 생성을 요청
                Then 즐겨찾기 생성됨
                When 즐겨찾기 목록 조회 요청
                Then 즐겨찾기 목록 조회됨
                When 즐겨찾기 삭제 요청
                Then 즐겨찾기 삭제됨
        ```

 

이 중에서 회원 정보 기능을 구현할 때 작성했던 인수 테스트를 예로 들어보겠습니다

회원 정보를 구현하기 전에 총 2가지 시나리오를 만들었습니다

    @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() {
        // given
        회원_등록됨(EMAIL, PASSWORD, AGE);
        ExtractableResponse<Response> loginResponse = 로그인_요청(EMAIL, PASSWORD);
        로그인_됨(loginResponse);
        String 토큰 = loginResponse.as(TokenResponse.class).getAccessToken();

        // when
        ExtractableResponse<Response> myInfoResponse = 나의_정보_조회를_요청(토큰);
        // then
        나의_정보가_조회됨(myInfoResponse);

        // when
        ExtractableResponse<Response> changeMyInfoResponse = 나의_정보_수정을_요청(토큰, NEW_EMAIL, NEW_PASSWORD, NEW_AGE);
        // then
        나의_정보가_수정됨(changeMyInfoResponse);
        String 새로운_토큰 = 나의_수정된_정보가_조회됨(NEW_EMAIL, NEW_PASSWORD);

        // when
        나의_정보_삭제를_요청(새로운_토큰);
        // then
        나의_정보가_삭제됨(새로운_토큰);
    }

이렇게 시나리오 단위로 묶인 통합 인수 테스트는 관리가 쉽습니다

코드의 변경이 있을 때, 단위 테스트는 해당 비지니스 로직만 검증할 수 있지만, 인수 테스트를 통해 실제 기능 동작을 시나리오 단위로 검증할 수 있습니다

 

시나리오 기반 ATDD 적용

이 미션을 완수하고 실무에서 운영하고 있는 API에 적용하면 좋을 것 같다는 생각을 했습니다
(글쓴이는 현재 검색 도메인에서 일을 하고 있습니다)

 

현재 실무에서 사용하는 API는 PC, 모바일, 앱을 통해 고객들의 요청을 받아 응답합니다

시나리오를 구성하기 전에 PC, 모바일, 앱 3가지의 Feature 를 구성했습니다

 

그리고 각 Feature에 시나리오들을 구성했습니다

PC를 예로 들어보겠습니다

아래처럼 검색 요청, 카테고리 페이지 접근 등 서비스에 맞게 다양한 시나리오로 구성할 수 있습니다 

    * PC
    ```
        Feature: PC 
            Scenario: PC에서 고객이 검색하고 응답을 받음
                Given 고객 설정
  
                When  검색을 함
                Then  정상적인 응답
  
                When  오타로 검색을 함
                Then  오타를 보정해준 응답
				....
                
          Scenario: PC에서 고객이 카테고리에 들어와 응답을 받음
                Given 고객 설정
  
                When  고객이 카테고리에 들어옴
                Then  정상적인 응답

                When  고객이 가격 정렬을 함
                Then  정렬된 결과를 응답
  
            

 

이렇게 이번 수업에서 배웠던 통합 인수 테스트를 실무에서 적용해보고 있습니다

작성된 인수 테스트는 API 빌드 전에 수행하여 시나리오를 검증하려고 합니다

 

Outside-In TDD

통합 인수 테스트 외에도 미션에서 많은 도움을 얻을 수 있었습니다

그 중 한가지는 TDD 접근법입니다

 

Outside-In과 Inside-Out 2가지로 접근할 수 있습니다

 

글쓴이는 Outside-In 방식을 선호합니다 

 

먼저 시나리오를 작성하고, 시나리오 기반으로 인수 테스트를 작성합니다
그리고 그 인수 테스트를 기반으로 빈 컨트롤러와 빈 서비스를 만듭니다
각 컨트롤러의 요청과 응답에 맞는 Request와 Response를 만들고 이를 기반으로 도메인을 만듭니다
이제 TDD를 통해서 도메인을 설계합니다
도메인 단위 테스트가 모두 성공한 후, 인수 테스트까지 성공하면 모든 기능이 완성됩니다

 

이번 미션에서도 많은 배움이 있었네요

다음 6주차는 레거시 코드를 리팩터링하는 미션입니다.

지금까지 배웠던 모든 것을 적용하는 미션이네요

 

 

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

 

댓글