티스토리 뷰

인프런 [Practical Testing: 실용적인 테스트 가이드] 를 들으며,
테스트 주도 개발과 Layered 계층에 따른 단위 테스트에 대한 개념과 느낀 점 정리

테스트 주도 개발 TDD (Test Driven Development)

 

테스트 주도 개발은 개발방법론의 실천 방안 중 하나이다.

  • 테스트 리사이클
    • Red : 요구사항 기준 테스트 코드 작성
    • Greed : 테스트를 통화하는 최소한의 코드를 작성
    • Blue : 테스트에 성공한 코드를 리팩토링
  • Red
    요구사항에 따른 최소 메서드를 생성하고 1차 테스트 코드를 실행한다.
    시작 페이지를 만들기 시작하는 단계로 비즈니스 로직 없이 공간만 있기 때문에, 테스트를 통과할 수 없다.
  • Green
    최소한의 비즈니스 로직을 포함하여 테스트를 성공시킨다.
    비즈니스 코드가 클린하게 작성될 필요는 없다.
    요구사항에 따른 기능이 정상적으로 진행되는지 확인한다.

    Green을 보면 자세한 비즈니스 로직을 추가하며 예외 테스트도 포함한다..
    이때부터 TDD의 장점을 느낄 수 있다.
  • Blue
    요구사항을 만족하는 코드가 완성되었다면 코드 리팩토링을 진행한다.
    비즈니스 로직을 효율적으로 수정하고 읽기 쉬운 코드로 정리한다.

 

TDD의 주된 작업은 Green을 빠르게 만족시키고, 계속 초록불을 확인하며 코드를 리팩토링 하는 것이다.
테스트를 완료한 다음 새로운 테스트 케이스를 확장시켜 나가며, 초록불을 늘려가면서 프로젝트의 규모를 키워간다.

 

내가 경험한 TDD의 장점

1) 오탈자, 괄호 누락 등 단순한 컴파일을 빠르게 수정한다.
2) 사용자 관점 코드 작성이 가능하다.
3) 코드를 최적화하고 새 기능을 추가하는 단계가 명확해진다. -> 유지보수하기 좋은 코드를 짤 수 있다.
4) 디버깅 횟수가 압도적으로 줄어 후반 개발 속도가 빠르다.
5) 코드 측정까지 진행한다면 높은 코드 커버리지를 기대할 수 있다.

 

TDD를 진행하면 무엇보다 테스트를 위한 코드를 작성하게 된다.
영향력이 큰 데이터값을 파라미터로 넣게 되면 테스트가 어려울 수 있다. 동시에 상위 계층에서의 연결이 어려울 수 있다.
persistence에서 TDD 방식의 개발을 쌓아가면, 마지막에는 읽기 쉽고 확장하기 좋은 코드가 완성될 수 있음을 느꼈다.

 

고통스러웠던 TDD의 단점

1) 초반이 정말 느리다.
2) 초록불이 항상 정답은 아니다.
3) 많은 테스트 케이스를 감당하며 프로젝트를 다루어야 한다.
4) 프로젝트가 커졌을 때, 무수히 많은 클래스를 체크해야 하는 것은 동일하다. (아직 익숙하지 않아서 그럴지도)

 

내가 생각하는 TDD의 가장 중요한 요소는 "코드 한 줄 추가 = 테스트 케이스 추가"이다.
TDD를 하려는 중간에 이 중점을 한 번이라도 놓친다면 결국 버그와 클래스 사이에 미아가 돼버리고 만다.

하지만 TDD가 주는 장점은 명확했고, 직접 강의를 따라 실습하며 이전 프로젝트를 회고할 수 있었다.

 

24년을 마무리하는 프로젝트에서 코드커버리지 측정을 처음 경험하였다.
방대한 비즈니스 로직을 따라가며 코드커버리지를 채우는 것은 단순 노동의 느낌이었다.

- 브랜치 머지마다 좋긴 했다.
- 다른 팀원의 코드를 확인할 때도 좋았다.
- 근데, "Mock으로 뻔한 값을 넣고 흐름만 보는데 이게 의미가 있을까?"

 

TDD 강의를 따라가면서 머릿속 코드커버리지 그래프가 100%까지 손쉽게 채울 수 있겠다는 기대감이 생겼다.
그리고 하나의 로직을 반복적으로 테스트하며 초기 개발단계에서 리팩토링을 동시 진행한다는 부분은
초록불을 볼 때마다 코드가 간결해지게 변하는 것을 눈으로 볼 수 있었다.

 

TDD를 사용해야 하는 순간

(한빛앤 | 주니어개발자를 위한 TPO for TDD)

  1. 처음 해보는 프로그램 주제
  2. 고객의 요구 사항이 바뀔 수 있는 프로젝트
  3. 개발하는 중에 코드를 많이 바꿔야 된다고 생각하는 경우
  4. 내가 개발하고 누가 유지보수 할지 모르는 경우

 

테스트 코드는 중요해!

TDD는 방법론이다. 적용할까 말 까는 언제나 해당 비즈니스의 상황에 따라 선택해야 한다.
모든 것은 단점이 있고, 또 명확한 장점도 있다.

 

하지만 테스트 코드는 그 자체로 중요하다.
TDD의 도입은 선택이지만 테스트 코드는 없어서는 안 되는 것 같다.

 

단위 테스트와 Layered Architecture

Persistence Layer 기준으로 단위테스트를 진행했다.
가장 밑에서부터 위로 TDD를 따라간다.

 

Persistence Layer Unit Test

Repository에 메서드가 추가될 때마다, 테스트 결과를 확인한다.
가장 작은 단위 테스트이다.

Persistence 계층은 비즈니스 로직을 가져서는 안 된다.
오직 CRUD만 관여한다.

 

@Repository public interface ProductRepository extends JpaRepository<Product, Long> {

	List findAllBySellingStatusIn(List sellingStatuses);
}
  @DisplayName("원하는 판매상태를 가진 상품들을 조회한다.")
  @Test
  void findAllBySellingStatusIn() {
    // given
    Product product1 = createProduct("001", HANDMADE, SELLING, "아메리카노", 4000);
    Product product2 = createProduct("002", HANDMADE, HOLD, "카페라떼", 4500);
    Product product3 = createProduct("003", HANDMADE, STOP_SELLING, "팥빙수", 7000);
    productRepository.saveAll(List.of(product1, product2, product3));

    // when
    List<Product> products = productRepository.findAllBySellingStatusIn(List.of(SELLING, HOLD));

    // then
    assertThat(products).hasSize(2)
        .extracting("productNumber", "name", "sellingStatus")
        .containsExactlyInAnyOrder(
            tuple("001", "아메리카노", SELLING),
            tuple("002", "카페라떼", HOLD)
        );
  }

 

Business Layer Unit Test

레포지토리를 포함하여 비즈니스 로직을 검증한다.
서비스 클래스의 메서드 명을 추가하면 return null부터 테스트 코드는 시작되어야 한다.
테스트 양이 가장 많다고 느꼈고, 복잡해지기 시작한다.

  • Red를 보는 메서드 생성 시작의 테스트 코드
  public ProductResponse getProductByNumber(String productNumber) {
    return null;
  }

 

    @DisplayName("상품 번호로 상품 한개를 조회한다.")
  @Test
  void getProductByNumber() {
    // given
    String productNumber = "001";

    Product product = createProduct("001", HANDMADE, SELLING, "아메리카노", 4000);
    productRepository.saveAll(List.of(product));

    // when
    String productName = productService.getProductByNumber(productNumber).getName();

    // then
    assertThat(productName).isEqualTo(productNumber);
  }

Presentaion Layer Test

오직 컨트롤러만 테스트한다.
Mock을 활용하여 서비스 클래스를 주입한다.

컨트롤러는 사용자의 값의 유효성을 검사한다. @Valid

  @PostMapping("/api/v1/products")
  public ApiResponse<ProductResponse> createProduct(@Valid @RequestBody ProductCreateRequest request) {
    return ApiResponse.ok(productService.createProduct(request));
  }
public class ProductCreateRequest {

    @NotNull(message = "상품 타입은 필수입니다.")
    ProductType type;

    @NotNull(message = "상품 판매상태는 필수입니다.")
    ProductSellingStatus sellingStatus;

    @NotBlank(message = "상품 이름은 필수입니다.")
    String name;

    @Positive(message = "상품 가격은 양수여야 합니다.")
    int price;
  }
@DisplayName("신규 상품을 등록한다.")
  @Test
  void createProduct() throws Exception {
    // given
    ProductCreateRequest request = ProductCreateRequest.builder()
            .type(ProductType.HANDMADE)
            .sellingStatus(ProductSellingStatus.SELLING)
            .name("아메리카노")
            .price(4000)
            .build();

    // when // then
    mockMvc.perform(
          post("/api/v1/products/new")
          .content(objectMapper.writeValueAsString(request))
          .contentType(MediaType.APPLICATION_JSON)
          )
          .andDo(print())
          .andExpect(status().isOk());
  }

 

마무리하며,

테스트 코드의 중요성을 인식해서 강의를 듣기 시작했다.
그래서 그런지 학습에 큰 어려움은 없었지만, TDD를 직접 도입하여 실습해 보는 경험은

"왜 우리가 테스트 코드를 작성해야 하는 가"를 몸으로 이해할 수 있는 시간이었다.

 

테스트 코드를 잘 짜고 싶다 생각했는데,
테스트할 수 있는 코드가 구현되어야 하는 것이었다.

 

 

 

 

주니어 개발자를 위한 TPO for TDD

총 강의 수 : 1강 총 강의 길이 : 1시간 수강권장기간 : 1개월 (30일) 보관기간 : 제한 없음 수강 권장 기간 : 30일

www.hanbitn.com

 

 

Practical Testing: 실용적인 테스트 가이드 강의 | 박우빈 - 인프런

박우빈 | , 실무에 맞는 올바른 테스트 코드 그 첫걸음이 되어드릴게요!  [사진] [임베딩 영상] 테스트 코드가 정말 그렇게 중요한가요? 🤔 그럼요! 테스트 코드 없이는 내가 만든 애플리케이션

www.inflearn.com

 

'Test' 카테고리의 다른 글

Stub이 뭐야? Test Double, Test Harness 그리고 Mockist Test  (0) 2025.04.03
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
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
글 보관함