티스토리 뷰

1. ATDD 기반으로 커머스 개발

최근 학습 과정에 성장에 정체가 느껴져 고민을 해 보았는데 요즘 회사를 제외하고는 직접 고민을 통해 문제를 해결하는 것보다는 강의나 책을 보며 따라서 작성하거나 책을 읽으며 개념적인 부분을 채우기만 했다는 생각이 들었다.

 

개인 적으로는 어느 한 쪽이 중요하다기보다는 간접 경험과 직접 경험의 적절한 밸런스를 통해 간접적인 내용을 체화시키는 과정이 필요하다고 생각하는데 계속해서 머릿속에 쑤셔 넣기만 하고 그것을 채화하는 것이 부족했다는 생각이 들었다. 때문에 학습 방법을 조금 바꾸어 책이나 다른 매체를 통해 학습한 내용을 사이드 프로젝트에 직접 적용해보는 과정을 가지기로했다.

 

사이드 프로젝트는 가상의 회사의 제품을 만든다는 생각을 가지고 진행하기로 했다. 경험할 수 있는 이슈와 그 이슈를 해결하는 방법을 제공하면서 문제 해결을 연습하고 그 과정 속에서 학습을 이어나가려고 한다. 시리즈로서 학습한 내용을 적용할 수 있는 가상의 이슈 혹은 요구사항을 통해 계속 확장해 나갈 예정이다.

 

📅 일정

22.11.13 ~ 22.11.21 

🎯 목적

ATDD와 TDD 개발 방법에 익숙해진다.
• Rest Assured를 통해 인수(시스템) 테스트를 진행한다.
• JUnit5 , Mock 등을 활용한 단위 테스트를 작성한다.

📢 요구사항

  • ATDD와 TDD를 적용한 작은 프로젝트를 시작한다.
  • 상품이 존재하며 이름과 가격을 가진다.
  • 상품의 생성이 가능하다.
  • 상품의 변경이 가능하다.
  • 상품의 삭제가 가능하다.
  • 상품을 단 건 조회가 가능하다.

 

 Rest Assured 

Rest Assured는 Java에 테스트 라이브러리 중 하나로 REST API를 테스트할 수 있도록 도와주는 도구이다. 기본적으로 인수 테스트는 블랙박스 성향을 가지는 것이 좋다. 즉, MockMVC와 같이 시스템 내부에서 테스트를 하는 것보다는 시스템 외부에서 호출하는 방식으로 E2E 테스트를 진행하는 것이 좋다. 

 

기본적으로 인수 테스트란? 인수 직전 즉, 개발자가 개발을 완료하고 QA를 요청하기 직전에 마지막 확인을 목적으로 유저가 사용한다는 생각의 테스트이다. 때문에 사용자가 사용하는 과정을 시나리오 형태로 만들어 시나리오를 테스트한다.  이는 시나리오 기반 테스트는 블랙박스 테스트가 적합한 이유이기도 하다.

 

Rest Assured는 이와 같은 블랙박스 기반의 인수 테스트 도구로 사용하기 좋다. 시스템 외부에서 요청을 가정하여 직접 요청하고 그에 대한 응답을 받아 그 응답을 검증하는 것으로 테스트를 진행한다.

 

우선 사용방법 확인을 위해 상품을 수정하는 요구사항에 대한 시나리오를 살펴보자

 

1
2
3
4
5
6
  /**
   * Feature : 상품 수정 기능
   *  Given : 상품이 생성되어 있다.
   *  When : 상품 수정 요청을 한다.
   *  Then : 상품이 수정 된다.
   */
cs

 

BDD(Behavior Driven Development) 방법을 통해 테스트를 구성한다. BDD는  Given-When-Then을 기준으로 테스트를 잘 작성하는 방법론이다. BDD를 사용하면 테스트 가독성이나 테스트 누락을 방지할 수 있다는 장점이 있고 또한 추후에 Cucumber 나 JBehave와 같은 BDD 도구를 사용하여 읽기 쉽고 재활용 가능한 인수 테스트 작성을 할 수도 있다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  @Test
  @DisplayName("상품 수정 기능")
  void modifyItem() {
    //given
    Map<StringString> params = 상품_매개변수_생성("닭볶음탕""18000");
    ExtractableResponse<Response> savedItem = 상품_생성(params);
 
    String updateLocation = savedItem.header("Location");
    Map<StringString> updateParams = 상품_매개변수_생성("닭볶음탕""20000");
 
    //when
    ExtractableResponse<Response> response = 상품_수정(updateLocation, updateParams);
 
    //then
    assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value());
    assertThat(response.jsonPath().getString("name")).isEqualTo(updateParams.get("name"));
    assertThat(response.jsonPath().getLong("price")).isEqualTo(Long.parseLong(updateParams.get("price")));
  }
 
cs

 

위 코드는 상품 수정에 대한 인수 테스트이다. 한글 이름의 메서드를 통해 테스트 전체를 읽기 쉽도록 구성하였다. 그리고 상품 수정, 삭제 등 여러 시나리오에서 사용될 수 있는 상품_생성과 같은 메서드들을 분리하여 하나의 스텝(단계)으로 구성해 두었다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ItemSteps {
  
 public static ExtractableResponse<Response> 상품_생성(Map<StringString> params) {
    ExtractableResponse<Response> response = RestAssured.given().log().all()
        .body(params)
        .contentType(MediaType.APPLICATION_JSON_VALUE)
        .when()
        .post("/items")
        .then().log().all()
        .extract();
    return response;
  }
 // ....
}
 
cs

 

상품 생성을 단계를 구성한 코드를 보면  ExtractableResponse  가 존재한다. 한국어로 해석하면 추출 가능한 응답이라는 의미를 가진다. 해당 객체는 응답 값에 Body, Header, ContentType 등 HTTP 프로토콜에 다양한 요소를 파싱 하여 제공해준다. 이러한 응답 값을 통해 시나리오를 통해 받은 응답에 상태 코드, 쿠키, 응답 바디 등으로 인수 테스트를 작성할 수 있다.

 

이러한 ExtractableResponse를 얻기 위해 RestAssured를 활용한다. 메서드를 체이닝 하는 형태로 HTTP Request를 만들 수 있다. 살펴보면 RestAssured 역시 Given-When-Then의 메서드를 가지고 BDD 방식의 테스트 코드 작성을 돕고 있다.

 

given에서는 HTTP의 요청을 만든다. Body와 ContentType 외에도 헤더나 쿠키를 설정해 요청을 만든다.

 

when에서는 HTTP 메서드와 그 요청을 받을 주소지를 결정한다. 

 

then에서는 요청을 보낸 후 응답 값을 받아 ExtractableResponse를 응답 값으로 받는다.

 

이렇게 RestAssured 라이브러리를 활용하여 시나리오 기반의 인수 테스트를 작성하였다. 다른 요구사항의 코드는 아래 링크에 Git 마일스톤을 통해 확인해보자. 

 

💡 자세한 RestAssured 사용 방법은 프로젝트 진행과정에 다양하게 사용해본 후 상세한 글로 정리해보도록 하겠습니다. 

 

 단위 테스트 

단위 테스트는 통합 테스트나 시스템 테스트와 다르게 최대한 작은 단위의 테스트를 의미한다. 보통의 경우 클래스 단위로 진행하게 되는데 클래스를 인스턴스화 하고 그 인스턴스의 퍼블리 메서드들을 테스트한다. 이때 의존 관계에 있는 클래스들은 인스턴스화 하지 않고 테스트 더블을 사용해서 대신한다. 때문에 너무 많은 의존관계가 존재하는 경우 많은 테스트 더블이 필요하고 테스트 작성이 힘들어진다.

 

TDD는 대표적으로 Outside-in(런던 스타일) TDD와 Inside-out(시카고 스타일) TDD로 나눌 수 있다. Outside-in은 시스템 진입점부터 테스트를 작성해 나가면서 안으로 들어오며 Inside-out은 테스트의 내부 도메인부터 시작해서 외부로 나가면서 테스트를 작성한다.

 

각각의 방법은 장단점이 존재하고 모든 상황에 적합한 방법은 존재하지 않기 때문에 장단점의 경우 아래 링크를 통해 확인해보자.

 

해당 프로젝트에서는 Inside-out 스타일의 TDD를 선택하였다. 목적이 성장인 사이드 프로젝트이기 때문에 상대적으로 오버 엔지니어링이 될 수 있다는 단점을 가지지만 어려워 익숙해지는 과정이 필요한 Inside-out 스타일을 선택하였다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  @Test
  @DisplayName("상품 수정 modify() 메소드 단위테스트")
  void modifyItem() {
    //given
    Item newItem = new Item("닭도리탕", 18000L);
    ItemModifyParams params = new ItemModifyParams("닭도리탕", 20000L);
 
    //when
    newItem.modify(params);
 
    //then
    assertThat(newItem.getName()).isEqualTo(params.getName());
    assertThat(newItem.getPrice()).isEqualTo(params.getPrice());
  }
cs

시카고 스타일의 TDD인 만큼 도메인 계층의 Item에서부터 테스트 작성과 개발을 시작한다. 간단하게 Item 객체를 생성하고 modify라는 퍼블릭 메서드를 테스트한다. 그 과정에 테스트 도구인 AssertJ를 활용하였다.

 

간단하게 값을 주입해 상태를 변경해주는 코드로 테스트를 생략할 수도 있었지만. 연습을 위한 과정이기 때문에 간단한 메서드에도 단위 테스트를 추가하였다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@ExtendWith({MockitoExtension.class})
class ItemServiceTest {
 
  public static final long ITEM_ID = 1L;
  @InjectMocks
  ItemService itemService;
 
  @Mock
  private ItemRepository itemRepository;
 
  @Test
  @DisplayName("상품 수정 - 성공 케이스")
  void modifyItem() {
    //given
    Item item = new Item("닭볶음탕", 18000L);
    ModifyItemDto modifyDto = new ModifyItemDto("닭볶음탕", 20000L);
    given(itemRepository.findById(any())).willReturn(Optional.of(item));
    //when
    ModifiedItemDto dto = itemService.modifyItem(ITEM_ID, modifyDto);
 
    //then
    assertThat(dto.getName()).isEqualTo(modifyDto.getName());
    assertThat(dto.getPrice()).isEqualTo(modifyDto.getPrice());
  }
 
  @Test
  @DisplayName("상품 수정 - 실패 케이스 ( Repository.find()가 null 일 때 )")
  void failModifyItem() {
    //given
    Item item = new Item("닭볶음탕", 18000L);
    ModifyItemDto modifyDto = new ModifyItemDto("닭볶음탕", 20000L);
    given(itemRepository.findById(any())).willReturn(Optional.empty());
    //when then
    assertThatCode(() -> itemService.modifyItem(ITEM_ID, modifyDto))
        .isInstanceOf(RuntimeException.class);
  }
 
//....
}
cs

 

Mockito를 활용한 테스트이다. ItemService가 의존하고 있는 ItemRepositroy 목킹해 단위 테스트를 작성하였다. 

 

👉 사용 방법이나 Mockito에 대한 내용은 우선 생략하고 프로젝트 진행 과정에 더 익숙해지면 필요에 따라 다른 글로 작성하겠습니다.

     전체 개발 내용의 경우 아래에 링크 중 마일스톤에서 확인해 볼 수 있다.

 

🔗 링크

 

🤔 회고

도메인이나 요구사항을 간단화하여 상대적으로 쉬웠던 것에 비해 일정이 너무 늘어진 부분이 있었다. 회사 일이 바쁘기도 했지만 사실 새로운 것을 넣기만 하려는 습관 때문에 적절한 시간을 해당 작업에 할당하지 못했기 때문으로 보인다. 추후에는 해당 내용에 대한 적절한 밸런스를 위해 일정을 조금 타이트하게 가져가서 수행할 수 있도록 할 필요가 있을 것으로 보인다.

 

👉 다음 일정

🔗 미니 커머스 헥사고날 아키텍처로 리팩터링

 

참고 자료

 

London vs Chicago TDD | DevLead.io

Published October 17, 2019by Doug Klugh It's an Integration, Not a Choice Now that you’ve mastered the basics of Test-Driven Development, consider the two primary schools of TDD.  The London School takes an outside-in, behavior-based approach, which fos

devlead.io

 

ATDD, 클린 코드 with Spring

 

edu.nextstep.camp

 

댓글