Practical Testing: 실용적인 테스트 가이드 강의 - 대시보드 | 인프런 (inflearn.com)

 

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

박우빈 | 이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을

www.inflearn.com

본 내용은 'Practical Testing: 실용적인 테스트 가이드' 라는 인프런 강의를 들으며 내 생각을 정리한 내용입니다.

 

Spring&JPA 기반 테스트

Layer Architecture

레이어드 아키텍쳐란, 아래와 같이 관심사를 3가지(혹은 4가지) 로 분류한 아키텍쳐를 의미한다. 

테스트는 각 레이어마다 분리해서 진행한다. 

 

 

Persistance Layer 테스트

비즈니스 가공 로직이 포함되어서는 안된다. 데이터에 대한 CRUD에만 집중한 레이어

@DataJpaTest
class StockRepositoryTest {
    @Autowired
    private StockRepository stockRepository;

    @DisplayName("상품번호 리스트로 재고를 조회한다.")
    @Test
    void findAllByProductNumberIn() {
        //given
        Stock stock1 = Stock.create("001", 1);
        Stock stock2 = Stock.create("002", 2);
        Stock stock3 = Stock.create("003", 3);
        stockRepository.saveAll(List.of(stock1, stock2, stock3));

        //when
        List<Stock> stocks = stockRepository.findAllByProductNumberIn(List.of("001", "002"));

        //then
        Assertions.assertThat(stocks).hasSize(2)
                .extracting("productNumber", "quantity")
                .containsExactlyInAnyOrder(
                        Tuple.tuple("001", 1),
                        Tuple.tuple("002", 2)
                );
    }

}

 

@DataJpaTest

JPA 관련 어노테이션이 적용된 클래스만 스캔하여 빈에 올린다. 때문에 모든 빈들을 올리는 @SpringBootTest 보다 로드되는 시간이 빠르다는 장점이 있다.

또한, 내장된 인메모리 DB를 사용한다.

 

하지만 JPA 관련 어노테이션만 스캔하기 때문에, QueryDsl 같은 보조 라이브러리를 사용한다면 추가적인 설정이 필요하다.

 

Business Layer 테스트

@ActiveProfiles("test")
@SpringBootTest
//@Transactional
class OrderServiceTest {
    @Autowired
    private OrderService orderService;
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private OrderProductRepository orderProductRepository;
    @Autowired
    private ProductRepository productRepository;
    @Autowired
    private StockRepository stockRepository;

    @AfterEach
    void tearDown(){
        orderProductRepository.deleteAllInBatch();
        productRepository.deleteAllInBatch();
        orderRepository.deleteAllInBatch();
        stockRepository.deleteAllInBatch();
    }

    @DisplayName("주문번호 리스트를 받아 주문을 생성한다.")
    @Test
    void createOrder() {
        //given
        Product product1 = creteProduct(HANDMADE,"001",1000);
        Product product2 = creteProduct(HANDMADE,"002",3000);
        Product product3 = creteProduct(HANDMADE,"003",5000);
        productRepository.saveAll(List.of(product1,product2,product3));

        OrderCreateRequest request = OrderCreateRequest.builder()
                .productNumbers(List.of("001", "002"))
                .build();
        //when
        LocalDateTime registeredDatetime = LocalDateTime.now();
        OrderResponse orderResponse = orderService.createOrder(request.toServiceRequest(), registeredDatetime);

        //then
        assertThat(orderResponse.getId()).isNotNull();
        assertThat(orderResponse)
                .extracting("registeredDateTime","TotalPrice")
                .contains(registeredDatetime,4000);
        assertThat(orderResponse.getProducts()).hasSize(2)
                .extracting("productNumber","price")
                .containsExactlyInAnyOrder(
                        tuple("001",1000),
                        tuple("002",3000)
                );
    }
    ...
}

 

tearDown 과 @Transactional, 그리고 JPA와 사용하는 @Transactional의 주의점

tearDown과 @Transactional 모두 전체 테스트를 수행할 때, 데이터가 전파되는것을 막기위한 방법이다. 

강사는 여기서 tearDown과 @Transactional의 차이를 제시해주었는데,

 

서비스단에 @Transactional이 없으면 변경감지를 하지 못해 실제로는 실패하지만, 

테스트단에 @Transactional이 있으면 변경감지를 하게 되기 때문에 테스트에서는 성공하는 케이스가 생긴다.

 

즉, 서비스에는 @Transactional이 없고, 테스트단에만 @Transactional이 있는 상황이 생길 수 있는데, 

실제로는 버그가 발생하지만 테스트에서는 이상없이 통과하는 케이스가 발생하게 된다라는 것이다.

 

결과적으로 테스트에서 @Transactional을 사용 할 때에는 서비스단에 @Transactional 적용을 했는지 여부를 한번에 파악할수 없기 때문에 주의해야한다고 한다.

 

서비스의 반환과 레포지토리의 반환이 동일하다면, 테스트를 전부 작성해야하는가에 대해

개인적으로 테스트 코드를 작성하면서 드는 고민 중 하나가 단순히 레포지토리에서 받아온 값을 반환하는 서비스라면, 동일한 결과를 테스트하는것인데 이걸 또 작성해야할까? 라는 의문이었다.

나는 이것에 대한 대답을 다음과 같이 정리했다. 

로직이 크게 없는 서비스와, 레포지토리의 테스트는 내용이 비슷하다.하지만
서비스가 발전할수록 기능이 추가되기 때문에 동일한 테스트라고 해도 작성하는것을 고민해보아야 한다.

 

도메인과 서비스에서 동일한 예외처리를 해야하는가에 대한 의문

아래 코드를 보면, 도메인과 서비스단에 동일한 예외(재고 부족)를 체크하고 있다. 이렇게 되면 동일한 예외를 중첩으로 체크하는데, 이유가 뭘까? 도메인에서만 예외를 터뜨려도 충분히 괜찮은 상황 아닐까?

public class Stock extends BaseEntity {
...

    public int deductQuantity(int quantity) {
        if(isQuantityLessThan(quantity)){
            throw new IllegalArgumentException("차감할 재고 수량이 없습니다.");
        }
        return this.quantity-=quantity;
    }
}
public class OrderService {
...

    private void deductStockQuantity(List<Product> products) {
        List<String> stockProductNumbers = extractStockProductNumbers(products);

        Map<String, Stock> stockMap = createStockMapBy(stockProductNumbers);

        Map<String, Long> productCountingMap = createCountingMapBy(stockProductNumbers);

        for (String stockProductNumber : new HashSet<>(stockProductNumbers)) {
            Stock stock = stockMap.get(stockProductNumber);
            int quantity = productCountingMap.get(stockProductNumber).intValue();
            if(stock.isQuantityLessThan(quantity)){
                throw new IllegalArgumentException("재고가 부족한 상품이 있습니다.");
            }
            stock.deductQuantity(quantity);
        }
    }
}

나는 답변을 다음처럼 정리했다.

클라이언트에게 주고싶은 메시지가 다르기 때문에 서비스단에서도 동일한 예외를 잡는다.
도메인에서 발생하는 예외는 범용적인 예외이고, 서비스단에서 발생하는 예외는 각 상황 맥락에 맞는 예외이기 때문에, 그에 맞는 메시지를 보내고 싶을 수 있다.

혹은 도메인 로직에서 커스텀 예외를 발생시키고, 외부에서 try-catch로 잡아 다시 원하는 예외 메시지를 전파하는 방법도 있다.

 

서비스단의 @Transactional(readOnly = true) - JPA 성능상 이점과 CQRS에 관하여

@Transactional(readOnly = true)가 되면 읽기 전용 즉, CRUD에서 R(조회) 만 동작하게 된다. 이렇게 되면 JPA에서 동작하는 변경감지같은 기능들이 동작할 필요가 없게되어 JPA 성능에 이점이 생긴다. 

 

CQRS란, command(업데이트) 와 query(읽기) 를 분리하는 패턴을 의미한다.

만약 서비스 자체를 읽기전용과 업데이트로 분리하게 된다면,

읽기전용 서비스는 슬레이브db, 업데이트 서비스는 마스터db에 배정하는 식으로 관리가 수월해진다.

 

Presentation Layer 테스트

파라미터에 대한 최소한의 검증을 수행

@WebMvcTest(controllers = ProductController.class)
class ProductControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    private ProductService productService;

   ...
   
    @DisplayName("판매 상품을 조회한다.")
    @Test
    void getSellingProducts() throws Exception {
        //given
        List<ProductResponse> responses = List.of();
        Mockito.when(productService.getSellingProducts()).thenReturn(responses);

        //when //then
        mockMvc.perform(
                get("/api/v1/products/selling")
        )
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value("200"))
                .andExpect(jsonPath("$.status").value("OK"))
                .andExpect(jsonPath("$.message").value("OK"))
                .andExpect(jsonPath("$.data").isArray());

    }
}

 

@WebMvcTest, MockMvc,  @MockBean

@WebMvcTest : mvc에 관해서만 슬라이스 테스트할 수 있는 어노테이션. @component, @Service 같은 빈들은 자동구성되지 않는다. 또한 MockMvc, SpringSecurity 도 자동 구성한다. 일반적으로 @MockBean과 함께 사용한다.

 

MockMvc : Mock 객체를 사용해 스프링 mvc 동작을 재현할 수 있는 스프링 테스트 프레임워크

 

@MockBean : Mock 객체를 생성하여 빈에 올린다. 이후 Mockito를 이용해 조건과 결과를 설정할 수 있다.

 

Mock 객체를 사용하는 이유

테스트를 하기위해서 준비해야할 것들(객체를 생성하고, 저장하고, 서비스를 시행하고, 등등)이 너무 많아지기 때문에 잘 동작하는것을 가정하고 테스트를 하고자 Mock(가짜) 객체를 사용한다.

하위 레이어가 상위 레이어를 모르는것이 가장 좋다.

컨트롤러 레이어 하위 dto를 서비스단에 그대로 넘기게 된다면,

하위레이어가 상위 레이어의 정보를 알게 되므로(의존관계 생성),

각각의 레이어를 분리해서 관리하고자 할 때 어려움이 생긴다.

컨트롤러에서 서비스로 전송할 때에는, 서비스 레이어 하위 dto로 받아 넘기는것이 좋다.

@RestController
@RequiredArgsConstructor
public class ProductController {
    private final ProductService productService;

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

}


@Getter
@NoArgsConstructor
public class ProductCreateRequest {

   ...
    public ProductCreateServiceRequest toServiceRequest() {
        return ProductCreateServiceRequest.builder()
                .type(type)
                .sellingStatus(sellingStatus)
                .name(name)
                .price(price)
                .build();
    }
}

@Transactional(readOnly = true)
@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository productRepository;
    @Transactional
    public ProductResponse createProduct(ProductCreateServiceRequest request) {
        String nextProductNumber = createNextProductNumber();

        Product product = request.toEntity(nextProductNumber);
        Product savedProduct = productRepository.save(product);

        return ProductResponse.of(savedProduct);
    }

  ...
}

 

@Valid 를 이용한 파라미터 검증

@NotNull 등과 같은 검증 어노테이션으로 파라미터를 검증할 수 있다.

@Getter
@NoArgsConstructor
public class ProductCreateRequest {

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

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

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

    @Positive(message = "상품 가격은 양수여야 합니다.")
    private int price;

    ...
}


@RestController
@RequiredArgsConstructor
public class ProductController {
    private final ProductService productService;

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

  ..
}

 

@WebMvcTest(controllers = ProductController.class)
class ProductControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    private ProductService productService;
    
    ...

    @DisplayName("신규 상품을 등록할 때 상풉 타입은 필수값이다.")
    @Test
    void createProductWithOutType() throws Exception {
        //given
        ProductCreateRequest request = ProductCreateRequest.builder()
                .sellingStatus(ProductSellingStatus.SELLING)
                .name("아메리카노")
                .price(4000)
                .build();

        //when //then
        mockMvc.perform(post("/api/v1/products/new")
                        .content(objectMapper.writeValueAsString(request))
                        .contentType(APPLICATION_JSON)
                )
                .andDo(print())
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("400"))
                .andExpect(jsonPath("$.status").value("BAD_REQUEST"))
                .andExpect(jsonPath("$.message").value("상품 타입은 필수입니다."))
                .andExpect(jsonPath("$.data").isEmpty());


    }

   ...
}

 

 

마치며

이번 섹션을 공부하면서, 지난날의 내가 테스트 코드를 짤 때, 짤 필요가 있는 코드와 없는 코드를 과연 구분했었는가에 대해서 고민을 하게 되었다. 또, TDD에 대해서 찾아보니 TDD에 대한 비판적인 시각도 꽤 많다는것을 알게 되었고, 그 부분도 이해가 되는 부분들이였다. 덕분에 무조건 적으로 받아들이는것이 아닌 비판적인 시각도 키워야겠다는 생각도 하게 되었다. 하지만 각자 장 단점이 존재 할 뿐 좋고 나쁘다는 없다고 생각하기 때문에 받아들이지 않을 필요도 없다고 생각한다.

 

 

참고

https://mangkyu.tistory.com/242

 

[Spring] 스프링부트 테스트를 위한 의존성과 어노테이션, 애플리케이션 컨택스트 캐싱(@SpringBootTes

스프링부트에서 테스트를 작성하기 위한 다양한 어노테이션(@SpringBootTest, @WebMvcTest, @DataJpaTest)들을 알아보도록 하겠습니다. 실제 테스트를 작성하는 방법은 이 포스팅을 참고해주세요. 1. 스프링

mangkyu.tistory.com

https://mson-it.tistory.com/16

 

[Spring] @SpringBootTest vs @DataJpaTest

테스트 코드를 작성하고 새로 작성된 테스트 코드를 돌려볼 때 실행 시간이 너무 오래 걸렸다. 이를 기존에 @SpringBootTest로 작성된 코드를 @DataJpaTest 변경하면서 해결할 수 있었는 데 변경하는 중

mson-it.tistory.com

https://jojoldu.tistory.com/320

 

@SpyBean @MockBean 의도적으로 사용하지 않기

보통 스프링 부트 관련 테스트 코드를 작성할때 @MockBean과 @SpyBean 를 사용했습니다. (참고: SpringBoot @MockBean, @SpyBean 소개) 복잡한 스프링 프로젝트에서도 원하는 코드만 아주 간단하게 Mock 처리를

jojoldu.tistory.com

https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.testing.spring-boot-applications.autoconfigured-spring-data-jpa

 

Spring Boot Reference Documentation

This section goes into more detail about how you should use Spring Boot. It covers topics such as build systems, auto-configuration, and how to run your applications. We also cover some Spring Boot best practices. Although there is nothing particularly spe

docs.spring.io

 

Practical Testing: 실용적인 테스트 가이드 강의 - 대시보드 | 인프런 (inflearn.com)

 

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

박우빈 | 이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을

www.inflearn.com

 

본 내용은 'Practical Testing: 실용적인 테스트 가이드' 라는 인프런 강의를 들으며 내 생각을 정리한 내용이다.

테스트 코드에 대해서 들어도 보았고, 중요하다는것은 알았지만,

어떤 형식으로 작성해야 할 지, 내가 제대로 이해하고 있는것은 맞는지 배우고자 수강했다.

 

나에게 테스트 코드란

필자는 테스트 코드라는 단어를 들어보기만 했지, 내 구현 코드를 위한 테스트코드를 실질적으로 작성해본적은 없었다.

그러다 작년 겨울 우아한테크코스 코딩 테스트에 참여하여 테스트 코드에 대해서 고민해보게 되면서 그 중요성을 조금씩 알아가게 되었다.

 

예전에는 나에게 테스트 코드란 '구현하기도 벅찬 나같은 사람이 아닌, 능력자들의 마지막 1% 같은 것'으로 보였지만,

지금은 '당장은 느려보일 수 있으나, 내 코드에 신뢰를 가질 수 있고, 내가 놓친 것들을 한번 더 생각하게 만드는 수단' 이라고 생각한다. 

테스트 코드는 왜 필요할까?

기능이 작을 때에는 수동으로 프로그램을 테스트를 어렵지 않게 수행 할 수 있다. 하지만, 기능이 점점 거치면서 수동으로 시행하기에는 너무 많은 테스트 케이스들이 생겨나게 된다. 이것들을 사람들이 일일이 테스트하기에는 시간과 비용이 들 뿐만 아니라, 실수도 필연적으로 생길 수 있다.

 

그렇기에 테스트 코드를 작성하여 사람이 수동으로 체크해야 할 사항들을 컴퓨터가 체크하게 함으로써 내 코드에 대한 신속한 피드백을 통해 코드 품질을 향상시킬 수 있다.

 

하지만 내가 무작정 테스트 코드를 작성한다고 해서, 수동 테스트의 단점이 전부 해결되는것은 아니다. 테스트 코드를 '잘' 작성해야지 수동 테스트가 가지는 어려움들을 해소할 수 있다. 

단위 테스트

단위 테스트(Unit test)

작은 코드 단위(클래스 or 메서드)를 독립적으로 검증하는 테스트를 의미한다.

또한, 테스트 방식 중 규모가 가장 작은 테스트 방식이다.

 

수동 테스트 ,자동화 테스트

수동 테스트는 사람이 직접 수동으로 프로그램을 실행하면서 검증하는 것 이라면, 

자동 테스트는 Junit5와 같은 테스트 프레임워크를 통하여 개발자가 작성한 케이스에 한하여 기계가 검증을 하는것을 의미한다.

 

또한, 모든 상황에서 수동 테스트 보다 자동화 테스트가 좋은 것은 아니기에

테스트 코드가 필요한 상황과 그렇지 않은 상황을 구분할 줄 아는 능력이 필요하다.

 

해피 케이스, 예외 케이스, 경계값 테스트

해피 케이스는 테스트 코드가 통과하는 케이스를 의미한다. 

우리가 아메리카노를 주문하는 코드를 작성한다고 할 때, 다음과 같은 해피 케이스 테스트 코드를 작성할 수 있다.

@Test
    void addSeveralBeverages() {
        CafeKiosk cafeKiosk = new CafeKiosk();
        Americano americano = new Americano();

        cafeKiosk.add(americano, 2);
        assertThat(cafeKiosk.getBeverages()).size().isEqualTo(2);
        assertThat(cafeKiosk.getBeverages().get(0)).isEqualTo(americano);
        assertThat(cafeKiosk.getBeverages().get(1)).isEqualTo(americano);

    }

 

 

예외 케이스는 코드에 예상치 못한 값을 넣었을 때, 그 값을 올바르게 처리할 수 있는지 테스트하는 케이스를 의미한다.

아래는 0잔의 아메리카노를 주문했을 때, 적절한 메시지가 출력되는지 확인하는 테스트 코드이다.

 @Test
    void addZeroBeverages() {
        CafeKiosk cafeKiosk = new CafeKiosk();
        Americano americano = new Americano();

        assertThatThrownBy(() -> cafeKiosk.add(americano, 0))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("음료는 1잔 이상 주문하실수 있습니다.");
    }

 

그리고 이런 케이스들을 테스트 할 때는 경계값을 테스트하는 것이 가장 중요하다.

 

예를들어서, 어떤 값이 3 이상 일 때, A라는 조건을 만족해야 한다면,

우리는 이 코드의 해피 케이스를 작성할 때, 4나 5 같은 수 보다는 경계값인 3으로 테스트 케이스를 작성함으로써,

지정한 범위 내에 코드가 정확히 동작하는지 파악 할 수 있다.

마찬가지로 예외 케이스를 작성 할 때, -1 이나 1을 이용하여 테스트 하기 보다는 경계값인 2를 테스트 하는것이 좋다. 

 

따라서 경계값이 존재하는 테스트 케이스는 경계값으로 테스트를 작성하는것이 중요하다.

 

테스트하기 어려운 부분을 분리하기

강의에서는 커피 가게의 오픈 시간을 예로 들어서 설명했다.

오전 10시 이전, 오후 10시 이후에는 "주문 시간이 아닙니다. 관리자에게 문의하세요." 라는 예외를 발생한다.

@Getter
public class CafeKiosk {
    private static final LocalTime SHOP_OPEN_TIME = LocalTime.of(10,0);
    private static final LocalTime SHOP_CLOSE_TIME = LocalTime.of(22,0);
    private final List<Beverage> beverages = new ArrayList<>();
    
    ...
    
        public Order createOrder(){
            LocalDateTime currentDateTime = LocalDateTime.now();
            LocalTime currentTime = currentDateTime.toLocalTime();

            if(currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)){
                throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요.");
            }
            return new Order(currentDateTime, beverages);
        }
    }

 

그리고 테스트하기 어려운 부분을 분리하지 않고 테스트 코드를 작성했다.

이 코드는 어쩔 때에는 통과하고, 어쩔 때에는 실패하게 된다. 이유가 뭘까?

 

CafeKiosk의 createOrder 내부 구현 때문이다.

createOrder는 현재 LocalDateTime.now()를 이용하여 주문을 생성하기 때문에, 우리가 테스트 하는 시간에 따라서 테스트가 통과할 수도, 실패할 수도 있게 된다.

@Test
    void createOrder() {
        CafeKiosk cafeKiosk = new CafeKiosk();
        Americano americano = new Americano();

        cafeKiosk.add(americano);

        assertThatThrownBy(() -> cafeKiosk.createOrder())
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("주문 시간이 아닙니다. 관리자에게 문의하세요.");
    }

 

이것을 해결하기 위해서는 테스트하기 어려운 영역(현재시간, LocalDateTime.now())를 기존 코드에서 제거하고,

메서드 외부에서 값을 주입받을 수 있도록 해야한다.

 public Order createOrder(LocalDateTime currentDateTime){
        LocalTime currentTime = currentDateTime.toLocalTime();

        if(currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)){
            throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요.");
        }
        return new Order(currentDateTime, beverages);
    }

 

이렇게 하면 우리가 원하는 시간대를 지정하여 테스트를 할 수 있게 된다.

 @Test
    void createOrderOutsideOpenTime() {
        CafeKiosk cafeKiosk = new CafeKiosk();
        Americano americano = new Americano();

        cafeKiosk.add(americano);

        assertThatThrownBy(() -> cafeKiosk.createOrder(LocalDateTime.of(2024, 3, 27, 9, 59)))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("주문 시간이 아닙니다. 관리자에게 문의하세요.");
    }

TDD: Test Driven Development

구현할 코드보다 테스트 코드를 먼저 작성하여 테스트가 구현을 주도하도록 하는 방법론을 의미한다.

 

쉽게 말하면 기능을 구현하고 테스트 코드를 작성하는 방식이 아닌,

테스트 코드를 먼저 작성하고, 테스트 결과를 피드백 받으면서 구현 코드를 완성해가는 방식이다. 

 

레드-그린-리펙토링

TDD를 수행하기 위한 기법이다. 레드  -> 그린 -> 리펙토링 순서대로 진행한다.

 

RED : 실패하는 테스트를 작성한다

실패하는 테스트를 작성하는 말이 책으로만 보면 무슨말인지 잘 이해가 되지 않았었다. 본 강의에서는 다음과 같은 예를 제시했다.

 

우리가 calculateTotalPrice라는 기능을 구현하려 할 때, 먼저 테스트 코드를 작성하여 기능이 없는 빈 메서드를 생성한다.

이런 상태에서 코드를 실행하면 RED,

즉, 실패하는 테스트를 작성하는것을 완료하게 된다.

 @Test
    void calculateTotalPrice() {

        CafeKiosk cafeKiosk = new CafeKiosk();
        Americano americano = new Americano();
        Latte latte = new Latte();

        cafeKiosk.add(americano);
        cafeKiosk.add(latte);

        int totalPrice = cafeKiosk.createTotalPrice();
        
        assertThat(totalPrice).isEqualTo(8500);

    }
    
public class CafeKiosk {
...

 	public int createTotalPrice() {
        return 0;
    }

}

GREEN : 테스트를 통과하는 최소한의 코드를 작성한다

강의에서는 다음과 같은 예를 제시했다.

극단적인 예이긴 하지만, 이것도 맞는 방법이라고 한다.

public class CafeKiosk {
...

 	public int createTotalPrice() {
        return 8500;
    }

}

 

 

REFACTORING : 구현 코드를 개선하여 테스트 통과를 유지한다.

이런식으로 코드를 리팩토링하여 여전히 테스트가 통과하도록 유지한다.

public int createTotalPrice() {
        return beverages.stream().mapToInt(Beverage::getPrice)
                .sum();
    }

 

 

1일차를 마치며

이 강의에서 좋은점이 각 섹션 마지막에는 키워드를 정리해 주는데 이 방식이 좋은것 같다. 추가적으로 알면 좋을 키워드들도 던져주면서 지식의 범위를 넓혀 주는데 아무래도 연관된 키워드다 보니 동기 부여가 되어서 , 그냥 무작정 자료들을 검색하는것들 보다 더 몰입하면서 공부할 수 있었다. 

 

참고 자료

Practical Testing: 실용적인 테스트 가이드 강의 - 대시보드 | 인프런 (inflearn.com)

 

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

박우빈 | 이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을

www.inflearn.com

https://testmanager.tistory.com/186

 

자동화 된 테스트 vs 수동 테스트 : 차이점

수동 테스트 란 무엇입니까?수동 테스트는 QA 분석가가 테스트를 수동으로 실행하는 소프트웨어 테스트입니다. 개발중인 소프트웨어에서 버그를 발견하기 위해 수행됩니다.수동 테스트에서 테

testmanager.tistory.com

 

+ Recent posts