TDD( Test Driven Development )

2024. 5. 27. 22:30카테고리 없음

TDD란 무엇인가?


'테스트 주도 개발'은 소프트웨어 개발 방법론 중 하나로

작은 단위의 테스트 케이스를 먼저 작성하고 이를 통과하는 코드를 작성하여 무한히 반복하는 것입니다.

  1. 실패하는 테스트 작성 (RED)
  2. 테스트를 통과하는 최소한의 코드 작성 (GREEN)
  3. 코드 리팩토링 (REFACTOR) 을 통해 중복 제거

 

💁TDD 도입 계기


첫 프로젝트에서 기획 시 설계해놓은 ERD와 기능 리스트를 기준으로 프로젝트를 진행했습니다.

메서드를 구현할 때 메서드가 실행되기 위해 필요한 파라미터는 자연스레 직접 참조를 하게 되었습니다.

 

이는 요구 사항이 변경될 때마다 중복된 코드를 추가하거나 기존 코드에 의존성을 더 많이 추가하게 되어

코드가 복잡해지고 유지보수가 어려워졌습니다.

 

또한 버그나 기능 추가로 인해 기존 로직을 수정하고 리팩토링을 진행하는 과정에서

해당 코드가 어느까지 영향을 미치게 되는지, 수정 시 이상이 없는지 바로 확인하기 어려워 이후 진행한 전체 테스트에서 에러의 원인을 정확하게 파악하기 어려웠습니다.

 

그래서 설계의 변화가 필요하다고 생각하였습니다.

 

 

🤔 왜 TDD를 도입해야 하는가?


1. 테스트가 가능한 코드라는 이상의 의미를 지닌다

 

테스트라는 기능 자체로도 의미있지만, 테스트 코드를 작성한다는 것 자체로 강점이 됩니다.

테스트 코드를 작성할 수 있다
-> 해당 엔티티 자체로 테스트가 가능하다
-> 다른 엔티티와 결합도가 낮고 객체 지향적인 설계를 지닌다라는 의미

 

 

2. 느슨한 결합을 위해

 

TDD를 진행하지않고 구현하게되면 요구사항을 만들기 위해 필요한 많은 정보를 알고 있는 것이

객체의 역할보다 더 많은 역할을 부여받거나 필요없는 결합을 만들 확률이 높일 것입니다.

 

예를 들어, 주문한 수량이 재고와 비교하여 주문 가능한 지 확인하는 기능을 만드려할 때

//orderService
public boolean checkOrderStock(OrderRequest orderRequest){
    long cafeMenuId = orderRequest.getCafeMenuId();
    int stock = orderRequest.getStock();

    CafeMenu cafeMenu = cafeMenuRepository.findById(cafeMenuId)
            .orElseThrow(() -> new RuntimeException("NOT_EXIST_MENU"));

    return cafeMenu.isStock(stock);
}

 

코드 작성 이후

OrderRequest 객체 대신 newOrderRequest 객체를 사용하도록 기능이 수정된다면?

 

newOrderRequest 파라미터만 변경되는 것이 아니라,

OrderService 클래스에 checkOrderStock 메서드를 사용하는 모든 코드가 수정되어야합니다.

 

특정 객체와의 종속성이 존재하다는 이유로 단일 책임 원칙도 위배하게 됩니다.

 

해당 메서드의 기능은 주문이 가능한지 여부를 판단하는 역할만을 가지면 된다는 점에서

아래처럼 테스트 코드를 이용하여 리팩토링한다면

특정 객체와의 결합도가 사라질 뿐 아니라 코드 자체의 필요한 역할만을 가지게 될 것입니다.

//orderServiceTest

//    주문 시 cafeMenu가 존재하지 않을때
    public void notExistCafeMenu(){
        long 카페메뉴_ID = 1L;
        orderService.order(카페메뉴_ID, null);

        assertThatExceptionOfType(RuntimeException.class)
                .isThrownBy(() -> orderService.order(카페메뉴_ID));
    }

    //    주문 시 수량이 stock보다 많을 때
    public void stock_부족(){
        long 카페메뉴_ID = 1L;
        int 수량 = 5;

        orderService.order(카페메뉴_ID, 수량);

        assertTrue(orderService.order(카페메뉴_ID, 수량));

    }
//orderService
public boolean order(long cafeMenuId, int stock){
    CafeMenu cafeMenu = cafeMenuRepository.findById(cafeMenuId)
            .orElseThrow(() -> new RuntimeException("NOT_EXIST_MENU"));

    return cafeMenu.isStock(stock);
}

 

 

💥 TDD의 단점


* 테스트 코드 작성 시간이 걸린다

 

처음 적용하는 경우, 익숙해지는데 시간이 필요하여 생산성이 떨어질 수 있습니다.

게다가 실제 코드를 구현하는 중에도 테스트 코드를 작성하는 시간이 추가로 필요하고,

복잡한 기능이나 다양한 상황을 고려하는 경우에는 더 많은 시간이 필요합니다.

 

 

하지만

 

실제로 기능을 개발하고 나서 버그를 찾기 위해 API를 호출하고 수정하는 프로세스를 반복하는 것이

결국에는 더 많은 시간이 소요될 수 있다는 점을 고려해볼 때

초기에 투자하는 시간은 전체적으로 생산성을 향상시키고 더 나은 코드 품질을 유지하는 데 도움이 될 것이라고 생각하였습니다.

 

 

💬 내가 적용한 방법


* DB 환경 분리

테스트하는 과정에서 실제 서비스 DB 데이터와 혼합된다면 테스트가 실패할 수 있기때문에

테스트만을 위한 H2를 사용하여 DB를 분리하였습니다.

 

application-test.yml을 추가로 작성하고

테스트 클래스에 어노테이션을 추가하여 테스트 클래스 수행 시 application-test.yml 을 돌리도록 하였습니다.

@ActiveProfiles("test")

 

* 테스트 명명 규칙

테스트 코드가 많아질수록 메서드명만으로 무슨 테스트인지 알아보기 어려워지고, 테스트 실패 시에도 식별이 어려워졌습니다.

@DisplayName 을 통해 먼저 정상/예외 테스트인지 표기하고, 테스트할 영역, 조건을 순서대로 나열하여 표시하였습니다.

@DisplayName("예외 : 아이디 자릿수 - 4-10자리 사이")

 

테스트의 목적을 명확히 표시하여 어떠한 상황에서 성공하고 실패하는지 쉽게 알아볼 수 있게 되었습니다.

 

* Test Fixture 사용

 

테스트 코드를 작성하다 보면 특정 상황이나 조건에 대해 여러 테스트 케이스에서 반복적으로 사용되는 경우가 많았습니다.

코드를 한 곳에서 관리하고, 여러 테스트 상황에 재사용하기 위해 Test Fixture를 사용하였고

테스트를 수행하는 데 필요한 초기화 데이터나 설정을 포함한 코드로 사용할 수 있습니다.

 

*테스트 속도 개선

 

🙆 TDD를 통해 이루고자 한 것


 ~~ 테스트가 실제 코드를