플레이데이터 - 39일차 Spring Boot Framework(4)

Kim Hyen Su·2023년 8월 24일

🌟테스트 코드 작성하기

  • 최근 애플리케이션을 개발 시 테스트 코드로 로직을 확인하는 과정이 점점 더 중요하게 여겨지고 있다.
  • 많은 개발자들이 어떻게하면 테스트 코드를 더 잘 작성하고 활용할 수 있는지 고민하고 있으며, 그 결과로 애자일 방법론 중에서 테스트 주도 개발(TDD)가 등장.

✅테스트 코드를 작성하는 이유

  • 개발과정에서 문제를 미리 발견 가능.

  • 리팩토링의 리스크가 줄어듦.

  • 애플리케이션을 가동해서 직접 테스트하는 것보다 테스트를 빠르게 진행할 수 있음.

  • 하나의 명세 문서로서의 기능을 수행.

    • 동작 검증을 위해 작성돼 있는 테스트 코드를 애플리케이션 코드와 비교하면서 보면 작성자의 의도가 파악되어 동료의 코드를 이해하기 쉽다.
  • 몇 가지 프레임워크에 맞춰 테스트 코드를 작성하면 좋은 코드를 생상 가능.

  • 코다가 작서된 목적을 명확하게 표현할 수 있으며, 불필요한 내용이 추가되는 것을 방지함.

✅단위 테스트와 통합 테스트

  • 단위 테스트 : 애플리케이션의 개별 모듈을 독립적으로 테스트하는 방식.

    • 테스트 대상의 범위를 기준으로 가장 작은 단위의 테스트 방식.
    • 일반적으로 메서드 단위로 테스트를 수행함.
  • 통합 테스트 : 애플리케이션을 구성하는 다양한 모듈을 결합해 전체적인 로직이 의도한 대로 동작하는지 테스트하는 방식.

    • 여러 모듈을 함께 테스트해서 정상적인 로직 수행이 가능한지를 확인.
    • 테스트 비용이 커지는 단점이 있음.
    • 테스트비용이란, 금전적인 비용을 포함해서 시간, 인력과 같은 개발에 필요한 것들을 포괄한 개념.

✅테스트 코드를 작성하는 방법

Given-When-Then 패턴

  • Given - When - Then 패턴은 테스트 주도 개발에서 파생된 BDD(Behavior-Driven-Development; 행위 주도 개발)을 통해 탄생한 테스트 접근 방식.
    • 위 패턴은 간단한 테스트로 여겨지는 단위 테스트에서는 잘 사용하지 않는데, 이는 불필요한 코드가 길어지기 때문이다.
    • 일반적으로 위 패턴을 작성하는 이유는 '명세' 문서의 역할을 수행한다.

Given

  • 테스트를 수행하기 전에 테스트에 필요한 환경을 설정하는 단계.
  • 테스트에 필요한 변수를 정의하거나 Mock 객체를 통해 특정 상황에 대한 행동을 정의함.

When

  • 테스트의 목적을 보여주는 단계.
  • 실제 테스트 코드가 포함되며, 테스트를 통한 결과값 출력.

Then

  • 테스트의 결과를 검증하는 단계.
  • When 단계에서 나온 결과값을 검증하는 작업 수행.

좋은 테스트를 작서하는 5가지 속성(F.I.R.S.T)

Fast

  • 테스트는 빠르게 수행돼야 하며, 일반적으로 목적을 단순하게 설정해서 작성하거나 외부 환경을 사용하지 않는 단위 테스트를 작성하는 것을 빠른 테스트라고 한다.

Isolated

  • 하나의 대상에 대해서만 수행돼야 한다.
  • 다른 테스트 코드와 상호작용을 최소화하거나 관리할 수 없는 외부소스를 사용하면 안된다.

Repeatable

  • 어떤 환경에서도 반복 테스트가 가능하도록 작성해야 한다.

Self-Validating

  • 그 자체만으로도 테스트의 검증이 완료돼야 한다.

Timely

  • 테스트하려는 애플리케이션 코드를 구현하기 전에 완성돼야 한다.
  • 다만, 테스트 주도 개발의 원칙을 따르는 테스트 작성 규칙으로 애플리케이션 개발이 수행되는 것이 아니라면 이 규칙을 제외하고 진행하기도 한다.

✅JUnit을 활용한 테스트 코드 작성

  • JUnit : 자바언어에서 사용되는 대표적인 테스트 프레임워크로써, 단위 테스트를 위한 도구를 제공한다.
  • 참고로 JUnit 5 버전은 스프링 부트 2.2 버전부터 사용이 가능하다.

JUnit의 세부 모듈

  • JUnit 5는 크게 Jupiter, Platform, Vintage 의 세 모듈로 구성.

JUnit Platform

  • JVM에서 테스트를 시작하기 위한 뼈대 역할.
  • 테스트 엔진의 인터페이스를 가진 모듈.
  • 테스트 엔진이란?
    • 테스트를 발견
    • 테스트 수행
    • 수행 결과를 보고
    • 각종 IDE와 연동을 보조

JUnit Jupiter

  • 테스트 엔진 API의 구현체.

JUnit Vintage

  • JUnit 3,4 에 대한 테스트 엔진 API를 구현, 이전 버전의 테스트 코드 실행 시 사용.

실습

  • DAO 레이어를 제외하고 서비스 레이어에서 바로 레포지토리를 사용하는 구조로 실습.

스프링 부트의 테스트 설정

  • 의존 추가
...
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>
...
  • spring-boot-starter-test 라이브러리에서 제공하는 대표 라이브러리.
    -
    • JUnit 5: 자바 애플리케이션의 단위 테스트를 지원.
    • SPring Test & Spring Boot Test: 스프링 부트 애플리케이션에 대한 유틸리티와 통합 테스트를 지원.
    • AssertJ: 다양한 단정문을 지원하는 라이브러리.
    • Hamcrest: Matcher를 지원하는 라이브러리.
    • Mockito: 자바 Mock 객체를 지원하는 프레임워크.
    • JSONassert: JSON용 단정문 라이브러리.
    • JsonPath: JSON용 XPath를 지원.

JUnit 생명주기

  • @Test: 테스트 코드를 포함한 메서드를 정의
  • @BeforeAll: 테스트를 시작하기 전에 호출되는 메서드를 정의
  • @BeforeEach: 각 테스트 메서드가 실행되기 전에 동작하는 메서드를 정의.
  • @AfterAll : 테스트를 종료하면서 호출되는 메서드를 정의
  • @AfterEach: 각 테스트 메서드가 종료되면서 호출되는 메서드를 정의
  • @Disabled: 지정된 테스트는 실행되지 않고 비활성화 됐다는 로그가 출력.

레이어별 다른 테스트

1. 컨트롤러 객체의 테스트

  • 컨트롤러 객체는 일반적으로 서비스 레이어의 객체를 의존한다.
  • 이는 컨트롤러만 테스트할 경우, 서비스 레이어의 객체는 외부 요인에 해당한다.
  • 독립적인 테스트 코드를 작성하기 위해서는 Mock 객체를 활용해야 한다.
package com.springboot.test.controller;


import com.google.gson.Gson;
import com.springboot.test.data.dto.ProductDto;
import com.springboot.test.data.dto.ProductResponseDto;
import com.springboot.test.service.impl.ProductServiceImpl;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(ProductController.class)
public class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    ProductServiceImpl service;

    @Test
    @DisplayName("MockMvc를 통한 Product 데이터 가져오기 테스트")
    void getProductTest() throws Exception{
        given(service.getProduct(123L)).willReturn(
          new ProductResponseDto(123L,"pen",5000,200)
        );

        String productId = "123";

        mockMvc.perform(
                get("/product?number=" + productId))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.number").exists())
                .andExpect(jsonPath("$.name").exists())
                .andExpect(jsonPath("$.price").exists())
                .andExpect(jsonPath("$.stock").exists())
                .andDo(print());

        verify(service).getProduct(123L);
    }
    
    @Test
    @DisplayName("Product 데이터 생성 테스트")
    void createdProductTest()throws Exception{
        given(service.saveProduct(new ProductDto("pen",5000,2000)))
                .willReturn(new ProductResponseDto(12315L,"pen",5000,2000));

        ProductDto dto = ProductDto.builder()
                .name("pen")
                .price(5000)
                .stock(2000)
                .build();

        Gson gson = new Gson();
        String content = gson.toJson(dto);

        mockMvc.perform( post("/product").content(content).contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.number").exists())
                .andExpect(jsonPath("$.name").exists())
                .andExpect(jsonPath("$.price").exists())
                .andExpect(jsonPath("$.stock").exists())
                .andDo(print());

        verify(service).saveProduct(new ProductDto("pen",5000,2000));
    }
}
  • @WebMvcTest(테스트 대상 클래스.class)
    웹에서 사용되는 요청과 응답에 대한 테스트를 수행할 수 있음.
    대상 클래스만 로드하여 테스트를 수행하며, 대상 클래스를 추가하지 않으면 @Controller, @RestController 등의 컨트롤러 관련 빈 객체가 모두 로드됨.

  • @MockBean
    Mock(가짜) 객체를 생성하여 주입해주는 역할 수행.
    Mock 객체는 실제 객체가 아니기 때문에 행위를 수행하지 않는다. 따라서 해당 객체는 개발자가 Mockito의 given() 메서드를 통해 동작을 정의해야 한다.

  • @Test
    테스트 코드가 포함되어 있다고 선언하는 어노테이션.

  • @Display
    테스트 메서드의 이름이 복잡해서 가독성이 떨어질 경우 이 어노테이션을 통해 테스트에 대한 표현을 정의가능.

  • 일반적으로 @WebMvcTest 어노테이션을 사용한 테스트는 슬라이스 테스트라고 한다. 이는 단위 테스트와 통합 테스트의 중간 개념으로 이해하면 된다.

profile
백엔드 서버 엔지니어

0개의 댓글