상세 컨텐츠

본문 제목

[스프링 2] 7장. 테스트코드 작성하기

24-25/Spring 2

by dalpaeng4 2024. 12. 27. 10:00

본문

728x90

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

  • 개발 과정에서 문제를 미리 발견할 수 있다
  • 리팩토링의 리스크가 줄어든다
  • 애플리케이션을 가동해서 직접 테스트하는 것보다 테스트를 빠르게 진행할 수 있다
  • 하나의 명세 문서로서의 기능을 수행한다
  • 몇 가지 프레임워크에 맞춰 테스트 코드를 작성하면 좋은 코드를 생산할 수 있다
  • 코드가 작성된 목적을 명확하게 표현할 수 있으며, 불필요한 내용이 추가되는 것을 방지한다.

 

테스트  기준은 여러 기준으로 분류할 수 있고ㅡ 테스트 대상 범위를 기준으로 구분하면 크게 단위 테스트, 통합 테스트로 구분된다.

 

 

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

Given-When-Then 패턴

불필요하게 코드가 길어지므로 간단한 테스트로 여겨지는 단위 테스트에서는 잘 사용하지 않는다.

명세 문서의 역할을 수행한다는 측면에서 많은 도움이 된다.

  • Given : 테스트를 수행하기 전에 필요한 환경을 설정하는 단계이다. 테스트에 필요한 변수를 정의하거나 Mock 객체를 통해 특정 상황에 대한 행동을 정의한다.
  • When : 테스트의 목적을 보여주는 단계이다. 실제 테스트 코드가 포함되며 테스트를 통한 결괏값을 가져온다.
  • Then : 테스트의 결과를 검증하는 단계이다. 일반적으로 When 단계에서 나온 결과값을 검증하는 작업을 수행한다. 결괏값이 아니더라도 이 테스트를 통해 나온 결과에서 검증해야 하는 부분이 있다면 이 단계에 포함된다.

F.I.R.S.T

테스트 코드를 작성하는데 도움이 될 수 있는 5가지 규칙을 의미한다.

단위 테스트에 적용할 수 있는 규칙이다.

  • Fast(빠르게) 
  • Isolated(고립된, 독립적)
  • Repeatable(반복 가능한)
  • Self-Validating(자가 검증)
  • Timely(적시에)

 

 

 Junit을 활용한 테스트 코드 작성 

JUnit은 자바 언어에서 사용되는 대표적인 테스트 프레임워크로서, 단위 테스트를 위한 도구를 제공한다.

단위 테스트 뿐만 아니라 통합 테스트를 수행할 수 있는 기능도 제공한다.

어노테이션 기반의 테스트 방식을 지원한다. -> JUnit을 사용하면 몇 개의 어노테이션만으로 간단하게 테스트 코드를 작성할 수 있다.

Junit을 활용하면 단정문(assert)문을 통해 테스트 케이스의 기댓값이 정상적으로 도출됐는지 검토할 수 있다는 장점이 있다.

크게 Jupiter, Platform, Vintabe 세 모듈로 구성된다.

 

 

 스프링 부트 프로젝트 

ProductServiceImpl 클래스 수정

@Service
public class ProductServiceImpl implements ProductService {

    private final Logger LOGGER = LoggerFactory.getLogger(ProductServiceImpl.class);
    private final ProductRepository productRepository;

    @Autowired
    public ProductServiceImpl(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Override
    public ProductResponseDto getProduct(Long number) {
        LOGGER.info("[getProduct] input number : {}", number);
        Product product = productRepository.findById(number).get();

        LOGGER.info("[getProduct] product number : {}, name : {}", product.getNumber(),
            product.getName());
        ProductResponseDto productResponseDto = new ProductResponseDto();
        productResponseDto.setNumber(product.getNumber());
        productResponseDto.setName(product.getName());
        productResponseDto.setPrice(product.getPrice());
        productResponseDto.setStock(product.getStock());

        return productResponseDto;
    }

    @Override
    public ProductResponseDto saveProduct(ProductDto productDto) {
        LOGGER.info("[saveProduct] productDTO : {}", productDto.toString());
        Product product = new Product();
        product.setName(productDto.getName());
        product.setPrice(productDto.getPrice());
        product.setStock(productDto.getStock());

        Product savedProduct = productRepository.save(product);
        LOGGER.info("[saveProduct] savedProduct : {}", savedProduct);

        ProductResponseDto productResponseDto = new ProductResponseDto();
        productResponseDto.setNumber(savedProduct.getNumber());
        productResponseDto.setName(savedProduct.getName());
        productResponseDto.setPrice(savedProduct.getPrice());
        productResponseDto.setStock(savedProduct.getStock());

        return productResponseDto;
    }

    @Override
    public ProductResponseDto changeProductName(Long number, String name) {
        Product foundProduct = productRepository.findById(number).get();
        foundProduct.setName(name);
        Product changedProduct = productRepository.save(foundProduct);

        ProductResponseDto productResponseDto = new ProductResponseDto();
        productResponseDto.setNumber(changedProduct.getNumber());
        productResponseDto.setName(changedProduct.getName());
        productResponseDto.setPrice(changedProduct.getPrice());
        productResponseDto.setStock(changedProduct.getStock());

        return productResponseDto;
    }

    @Override
    public void deleteProduct(Long number) {
        productRepository.deleteById(number);
    }
}

 

Product Entity 수정

@Entity
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString(exclude = "name")
@Table(name = "product")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long number;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private Integer price;

    @Column(nullable = false)
    private Integer stock;

    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;

}

 

testImplementation 'org.springframework.boot:spring-boot-starter-test'

스프링 부트에서 제공하는 spring-boot-starter-test 라이브러리는 JUnit, Mokito, assertJ 등의 다양한 테스트 도구를 제공한다. 또한 자동 설정ㅇ을 지원한다.

 

 

 JUnit의 생명 주기 

  • @Test : 테스크 코드를 포함한 메서드를 정의한다.
  • @BeforeAll : 테스트를 시작하기 전에 호출되는 메서드를 정의한다.
  • @BeforeEach : 각 테스트 메서드가 실행되기 전에 동작하는 테스트를 정의한다. 
  • @AfterAll : 테스트를 종료하면서 호출되는 메서드를 정의한다.
  • @AfterEach : 각 테스트 메서드가 종료되면서 호출되는 메서드를 정의한다. 
public class TestLifeCycle {

    @BeforeAll
    static void beforeAll() {
        System.out.println("## BeforeAll Annotation 호출 ##");
        System.out.println();
    }

    @AfterAll
    static void afterAll() {
        System.out.println("## afterAll Annotation 호출 ##");
        System.out.println();
    }

    @BeforeEach
    void beforeEach() {
        System.out.println("## beforeEach Annotation 호출 ##");
        System.out.println();
    }

    @AfterEach
    void afterEach() {
        System.out.println("## afterEach Annotation 호출 ##");
        System.out.println();
    }

    @Test
    void test1() {
        System.out.println("## test1 시작 ##");
        System.out.println();
    }

    @Test
    @DisplayName("Test Case 2!!!")
    void test2() {
        System.out.println("## test2 시작 ##");
        System.out.println();
    }

    // Disabled Annotation : 테스트를 실행하지 않게 설정하는 어노테이션
    @Test
    @Disabled
    void test3() {
        System.out.println("## test3 시작 ##");
        System.out.println();
    }

}

 

## beforeEach Annotation 호출 ##

## test1 시작 ##

## afterEach Annotation 호출 ##

## beforeEach Annotation 호출 ##

## test2 시작 ##

## afterEach Annotation 호출 ##

## afterAll Annotation 호출 ##

 

위 테스트 코드를 실행하면 다음과 같은 로그가 출력된다.

@BeforeAll@AfterAll 어노테이션이 지정된 메서드는 전체 테스트 동작에서 처음과 마지막에만 각각 수행된다.

@BeforeEach@AfterEach 어노테이션이 지정된 메서드는 각 메서드가 실행될 때마다 실행되어 테스트 메서드 기준으로 실행된다.

test3()는 @Disabled 애노테이션을 지정했는데, 이 애노테이션은 테스트를 비활성화한다. 따라서 test3()은 실행되지 않는다.

 

 

 ProductController의 getProduct()와 createProduct() 메서드에 대한 테스트 코드 작성 

@RestController
@RequestMapping("/product")
public class ProductController {

    private final Logger LOGGER = LoggerFactory.getLogger(ProductController.class);

    private final ProductService productService;

 

ProductCobntroller는 ProductService 객체를 의존성 주입받는다. 테스트하는 입장에서 ProductController만 테스트하고 싶다면 ProuctService는 외부 요인에 해당한다. 따라서 독립적인 테스트 코드를 작성하기 위해서는 Mock 객체를 활용해야 한다. 

 

getProduct() 테스트코드 작성 

@WebMvcTest(ProductController.class)
class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

    // ProductController에서 잡고 있는 Bean 객체에 대해 Mock 형태의 객체를 생성해줌
    @MockBean
    ProductServiceImpl productService;

    // 예제 7.6
    // http://localhost:8080/api/v1/product-api/product/{productId}
    @Test
    @DisplayName("MockMvc를 통한 Product 데이터 가져오기 테스트")
    void getProductTest() throws Exception {

        // given : Mock 객체가 특정 상황에서 해야하는 행위를 정의하는 메소드
        given(productService.getProduct(123L)).willReturn(
            new ProductResponseDto(123L, "pen", 5000, 2000));

        String productId = "123";

        // andExpect : 기대하는 값이 나왔는지 체크해볼 수 있는 메소드
        mockMvc.perform(
                get("/product?number=" + productId))
            .andExpect(status().isOk())
            .andExpect(jsonPath(
                "$.number").exists()) // json path의 depth가 깊어지면 .을 추가하여 탐색할 수 있음 (ex : $.productId.productIdName)
            .andExpect(jsonPath("$.name").exists())
            .andExpect(jsonPath("$.price").exists())
            .andExpect(jsonPath("$.stock").exists())
            .andDo(print());

        // verify : 해당 객체의 메소드가 실행되었는지 체크해줌
        verify(productService).getProduct(123L);
    }
}

 

  • @WebMvcTest(테스트 대상 클래스.class)
    웹에서 사용되는 요청과 응답에 대한 테스트를 수행할 수 있다. 대상 클래스만 로드해 테스트를 수행하며, 만약 대상 클래스를 추가하지 않으면 @Controller, @RestController, @ControllerAdvice 등의 컨트롤러 관련 빈 객체가 모두 로드된다. @SpringBootTest보다 가볍게 테스트하기 위해 사용한다
  • @MockBean
    @MockBean은 실제 빈 객체가 아닌 Mock(가짜) 객체를 생성해서 주입하는 역할을 수행한다. @MockBean이 선언된 객체는 실제 객체가 아니기 때문에 실제 행위를 수행하지 않는다. 그렇기 때문에 해당 객체는 개발자가 Mockito의 given() 메서드를 통해 동작을 정의해야 한다.
  • @Test
    테스트 코드가 포함돼 있다고 선언하는 어노테이션이며, JUnit Jupiter에서는 이 어노테이션을 감지해서 테스트 계획에 포함시킨다.
  • @DisplayName
    테스트 메서드의 이름이 복잡해서 가독성이 떨어질 경우 이 어노테이션을 통해 테스트에 대한 표현을 정의할 수 있다.

 

일반적으로 @WebMvcTest 어노테이션을 사용한 테스트는 슬라이스(Slice) 테스트라고 부른다.
슬라이스 테스트는 단위 테스트와 통합 테스트의 중간 개념으로, 레이어드 아키텍처를 기준으로 각 레이어별로 나누어 테스트를 진행한다는 의미이다. 단위 테스트를 수행하기 위해서는 모든 외부 요인을 차단하고 테스트를 진행해야 하지만 컨트롤러는 개념상 웹과 맞닿은 레이어로서 외부 요인을 차단하고 테스트하면 의미가 없기 때문에 슬라이스 테스트를 진행하는 경우가 많다.

 

Mockito 라이브러리는 given() 메서드를 통해 Mock 객체에 특정 상황에서 어떤 메서드를 실행할 때 어떤 결과를 반환해야 하는지 미리 지정할 수 있다.

perform() 메서드를 통해 MockMvc 객체의 HTTP 요청을 수행하며 결과를 검증한다. 해당 메서드는 HTTP 요청의 다양한 메서드(GET, POST, PUT, DELETE 등)를 처리할 수 있다. 테스트 코드에서는 URL, 요청 데이터와 같은 필수 정보를 설정한다. perform() 메서드는 결과값을 ResultActions 객체로 반환하는데, 이 객체는 추가적인 검증 메서드를 실행할 수 있게 돕는다. 예를 들어, andExpect() 메서드를 사용해 반환값을 검증하거나, 예상 결과값을 설정할 수 있다. andExpect 메서드와 함께 사용되는 ResultMatcher 객체를 통해 반환된 응답의 상태 코드나 데이터 값을 확인할 수 있다.

 

요청과 응답의 값을 더 구체화할 때는 andDo() 메서드를 사용한다. andDo 메서드는 테스트 결과를 출력하거나 로그로 남기는 역할을 한다. 이어서 MockMvcResultMatchers 유틸리티를 사용해 HTTP 응답 상태 코드나 반환된 결과값을 검증할 수 있다.

 

verify() 메서드를 통해 특정 메서드가 호출되었는지 확인한다. 이는 테스트의 끝단에서 중요한 검증 작업으로, given() 메서드에 정의된 동작이 제대로 수행되었는지 점검하는 역할을 한다.

 

 

createProduct() 테스트코드 작성 

implementation 'com.google.code.gson:gson:2.10.1'

 

build.gradle 기준 Gson 의존성 추가 

    @Test
    @DisplayName("Product 데이터 생성 테스트")
    void createProductTest() throws Exception {
        //Mock 객체에서 특정 메소드가 실행되는 경우 실제 Return을 줄 수 없기 때문에 아래와 같이 가정 사항을 만들어줌
        given(productService.saveProduct(new ProductDto("pen", 5000, 2000)))
            .willReturn(new ProductResponseDto(12315L, "pen", 5000, 2000));

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

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

        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(productService).saveProduct(new ProductDto("pen", 5000, 2000));
    }

 

given() 메서드를 통해 ProductService의 saveProduct() 메서드의 동작 규칙을 설정하고, 테스트에 필요한 객체를 생성한다. 실제 테스트를 수행하는 부분은 리소스 생성 기능을 테스트하기 때문에 post 메서드를 통해 URL을 구성한다. 그리고 @RequestBody의 값을 넘겨주기 위해 content() 메서드에 DTO의 값을 담아 테스트를 진행한다. 마지막으로 POST 요청을 통해 도출된 결과값에 대해 각 항목이 존재하는지 jsonPath()와 .exists()를 통해 검증한다. 검증한 결과, 대응하는 값이 없다면 오류가 발생한다.

 

 

 ProductService의 getProduct()와 createProduct() 메서드에 대한 테스트 코드 작성 

    @Test
    void getProductTest() {
        // given
        Product givenProduct = new Product();
        givenProduct.setNumber(123L);
        givenProduct.setName("펜");
        givenProduct.setPrice(1000);
        givenProduct.setStock(1234);

        Mockito.when(productRepository.findById(123L))
            .thenReturn(Optional.of(givenProduct));

        // when
        ProductResponseDto productResponseDto = productService.getProduct(123L);

        // then
        Assertions.assertEquals(productResponseDto.getNumber(), givenProduct.getNumber());
        Assertions.assertEquals(productResponseDto.getName(), givenProduct.getName());
        Assertions.assertEquals(productResponseDto.getPrice(), givenProduct.getPrice());
        Assertions.assertEquals(productResponseDto.getStock(), givenProduct.getStock());

        verify(productRepository).findById(123L);
    }

 

@SpringBootTest, @WebTest를 사용하지 않고 단위 테스트를 수행하는 대신 직접 객체를 생성해 테스트를 진행했다. Mockito.mock() 메서드를 통해 Mock 객체로 ProductRepository를 주입받았다.

 

이후 테스트 대상인 ProductService 클래스에서 사용하는 메서드를 실행하는 테스트이다.

    @Test
    void saveProductTest() {
        // given
        Mockito.when(productRepository.save(any(Product.class)))
            .then(returnsFirstArg());

        // when
        ProductResponseDto productResponseDto = productService.saveProduct(
            new ProductDto("펜", 1000, 1234));

        // then
        Assertions.assertEquals(productResponseDto.getName(), "펜");
        Assertions.assertEquals(productResponseDto.getPrice(), 1000);
        Assertions.assertEquals(productResponseDto.getStock(), 1234);

        verify(productRepository).save(any());
    }

 

  • any(): Mockito의 ArgumentMatchers를 통해 조건과 무관하게 특정 메서드가 호출되는지 확인할 때 사용한다.
  • returnsFirstArg(): 메서드 실행 시 첫 번째 인자를 반환하도록 설정한다.
  • verify(): save() 메서드가 호출되었는지를 검증한다.

ProductService가 정상적으로 동작하며, ProductRepository가 올바르게 호출되었는지를 검증한다.
Mock 객체를 사용했기 때문에 외부 데이터베이스에 접근하지 않아도 테스트를 수행할 수 있다.

 

Mock 객체를 직접 생성하지 않고 @MockBean 어노테이션을 사용해 스프링 컨테이너에 Mock 객체를 주입받는 방법

@ExtendWith(SpringExtension.class)
@Import({ProductServiceImpl.class})
class ProductServiceTest2 {

    @MockBean
    ProductRepository productRepository;

    @Autowired
    ProductService productService;

    // 테스트 코드 작성
}

 

1. @MockBean을 사용하는 것은 스프링에 Mock 객체를 등록해서 주입받은 형식이며,

2. @MockBean.mock()을 사용하는 방식은 스프링 빈에 등록하지 않고 직접 객체를 초기화해서 사용하는 방식이다.

 

두 방식 모두 테스트 속도에는 큰 차이가 없지만 스프링을 사용하지 않는 Mock 객체를 직접 생성하는 방식이 더 빠르게 동작한다. 

 

 리포지토리 객체의 테스트 

 

JpaRepository를 상속받아 기본적인 쿼리 메서드들을 사용할 수 있다.

그러나  findById(), save() 같은 레포지토리의 기본 메서드는 테스트 검증을 마치고 제공된 것이므로 이를 테스트할 필요는 없다.

 

단위 테스트의 경우 외부 요인에 해당하는 데이터베이스를 제외할 수 있다.

만약 데이터베이스를 연동할 경우 테스트 과정에서 데이터베이스에 테스트 데이터가 적재되므로 테스트 데이터를 제거하는 코드까지 포함하여 작성하는 것이 좋다.

 

데이터베이스에 값을 저장하는 테스트 코드

@DataJpaTest
public class ProductRepositoryTestByH2 {

    @Autowired
    private ProductRepository productRepository;

    @Test
    void saveTest() {
        // given
        Product product = new Product();
        product.setName("펜");
        product.setPrice(1000);
        product.setStock(1000);

        // when
        Product savedProduct = productRepository.save(product);

        // then
        assertEquals(product.getName(), savedProduct.getName());
        assertEquals(product.getPrice(), savedProduct.getPrice());
        assertEquals(product.getStock(), savedProduct.getStock());
    }
 }

 

@DataJpaTest 어노테이션 사용

  • JPA와 관련된 설정만 로드해서 테스트를 진행
  • 기본적으로 @Transactional 어노테이션을 포함하고 있어 테스트 코드가 종료되면 자동으로 데이터베이스의 롤백이 진행
  • 기본 값으로 임베디드 데이터베이스 사용. 다른 데이터베이스를 사용하려면 별도의 설정을 거쳐 사용 가능

@DataJpaTest 어노테이션을 선언했기 때문에 리포지토리를 정상적으로 주입받을 수 있게 된다.

 

데이터 조회에 대한 테스트 코드

@Test
void selectTest() {
    // given
    Product product = new Product();
    product.setName("펜");
    product.setPrice(1000);
    product.setStock(1000);

    Product savedProduct = productRepository.saveAndFlush(product);

    // when
    Product foundProduct = productRepository.findById(savedProduct.getNumber()).get();

    // then
    assertEquals(product.getName(), foundProduct.getName());
    assertEquals(product.getPrice(), foundProduct.getPrice());
    assertEquals(product.getStock(), foundProduct.getStock());
}

 

조회를 위해서는 데이터베이스에 테스트 데이터를 추가해야 한다.

-> given 절에서 객체를 데이터베이스에 저장하는 작업을 수행했다.

조회 메서드를 호출하여 테스트를 진행하고 코드에서 데이터를 비교하여 검증을 수행한다.

 

위 코드는 h2 데이터베이스에서 실행하기 위한 코드이며, 마리아DB에서 테스트하기 위해서는 별도의 설정이 필요하다.

테스트 데이터베이스 변경을 위한 어노테이션 추가 

@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
class ProductRepositoryTest {

    @Autowired
    private ProductRepository productRepository;

    @Test
    void save() {
        // given
        Product product = new Product();
        product.setName("펜");
        product.setPrice(1000);
        product.setStock(1000);

        // when
        Product savedProduct = productRepository.save(product);

        // then
        assertEquals(product.getName(), savedProduct.getName());
        assertEquals(product.getPrice(), savedProduct.getPrice());
        assertEquals(product.getStock(), savedProduct.getStock());
    }

}

 

replace = Replace.ANY일 경우 임베디드 메모리 데이터베이스를 사용한다.

replace = Replace.NONE로 변경하면 애플리케이션에서 실제로 사용하는 데이터베이스로 테스트를 할 수 있다.

 

@DataJpaTest를 사용하지 않고 @SpringBootTest 어노테이션으로 테스트 진행

package com.springboot.demo.data.repository;

import com.springboot.demo.data.entity.Product;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

@SpringBootTest
public class ProductRepositoryTest2 {

    @Autowired
    ProductRepository productRepository;

    @Test
    public void basicCRUDTest() {
        /* create */
        // given
        Product givenProduct = Product.builder()
            .name("노트")
            .price(1000)
            .stock(500)
            .build();

        // when
        Product savedProduct = productRepository.save(givenProduct);

        // then
        Assertions.assertThat(savedProduct.getNumber())
            .isEqualTo(givenProduct.getNumber());
        Assertions.assertThat(savedProduct.getName())
            .isEqualTo(givenProduct.getName());
        Assertions.assertThat(savedProduct.getPrice())
            .isEqualTo(givenProduct.getPrice());
        Assertions.assertThat(savedProduct.getStock())
            .isEqualTo(givenProduct.getStock());

        /* read */
        // when
        Product selectedProduct = productRepository.findById(savedProduct.getNumber())
            .orElseThrow(RuntimeException::new);

        // then
        Assertions.assertThat(selectedProduct.getNumber())
            .isEqualTo(givenProduct.getNumber());
        Assertions.assertThat(selectedProduct.getName())
            .isEqualTo(givenProduct.getName());
        Assertions.assertThat(selectedProduct.getPrice())
            .isEqualTo(givenProduct.getPrice());
        Assertions.assertThat(selectedProduct.getStock())
            .isEqualTo(givenProduct.getStock());

        /* update */
        // when
        Product foundProduct = productRepository.findById(selectedProduct.getNumber())
            .orElseThrow(RuntimeException::new);

        foundProduct.setName("장난감");

        Product updatedProduct = productRepository.save(foundProduct);

        // then
        assertEquals(updatedProduct.getName(), "장난감");

        /* delete */
        // when
        productRepository.delete(updatedProduct);

        // then
        assertFalse(productRepository.findById(selectedProduct.getNumber()).isPresent());
    }
}

 

@SpringBootTest 어노테이션을 활용하면 스프링의 모든 설정을 가져오고 빈 객체도 전체를 스캔하기 때문에 의존성 주입에 대해 고민할 필요가 없이 테스트가 가능하다.

다만, 테스트 속도가 느리기 때문에 다른 방법으로 테스트할 수 있다면 대안을 고려해보는 것이 좋다.

 

 JaCoCo를 활용한 테스트 커버리지 확인 

코드 커버리지는 소프트웨어의 테스트 수준이 충분한지를 표현하는 지표 중 하나이다.

테스트를 진행했을 때 대상 코드가 실행됐는지 표현하는 방법으로도 사용된다.

 

커버리지를 확인하기 위해 사용하는 도구 중 JaCoCo가 가장 보편적으로 사용된다.

Java Code Coverage의 약자로 JUnit 테스트를 통해 애플리케이션의 코드가 얼마나 테스트되었느지 Line과 Branch를 기준으로 한 커버리지로 리포트한다.

 

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.7</version>
    <configuration>
        <excludes>
            <exclude>**/ProductServiceImpl.class</exclude>
        </excludes>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>jacoco-report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
        <execution>
            <id>jacoco-check</id>
            <goals>
                <goal>check</goal>
            </goals>
            <configuration>
                <rules>
                    <rule>
                        <element>BUNDLE</element>
                        <limits>
                            <limit>
                                <counter>INSTRUCTION</counter>
                                <value>COVEREDRATIO</value>
                                <minimum>0.80</minimum>
                            </limit>
                        </limits>
                        <element>METHOD</element>
                        <limits>
                            <limit>
                                <counter>LINE</counter>
                                <value>TOTALCOUNT</value>
                                <maximum>50</maximum>
                            </limit>
                        </limits>
                    </rule>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

 

index.html이 아래처럼 떠서 진행하지 못했습니다. 책 참고해주세요!

 

 

 테스트 주도 개발 

TDD(Test Driven Development) 반복 테스트를 이용한 소프트웨어 개발 방법론으로서 테스트 코드를 먼저 작성한 후 테스트를 통과하는 코드를 작성하는 과정을 반복하는 소프트웨어 개발 방식이다. 

 

총 3개의 단계로 개발 주기를 표현한다.

  • 실제 테스트 작성: 실패하는 경우의 테스트 코드를 먼저 작성
  • 테스트를 통과하는 코드 작성: 테스트 코드를 성공시키기 위한 실제 코드를 작성
  • 리팩토링: 중복 코드를 제거하거나 일반화하는 리팩토링을 수행

 

일반적인 개발 방법은 설계를 진행한 후 그에 맞게 애플리케이션 코드를 작성하고 마지막으로 테스트 코드를 작성하는 흐름으로 진행된다. 

반면 테스트 주도 개발에서는 설계 이후 바로 테스트 코드를 작성하고 애플리케이션 코드를 작성한다는 점에서 차이가 있다.

 

테스트 주도 개발의 효과

  • 디버깅 시간 단축
  • 생산성 향상
  • 재설계 시간 단축
  • 기능 추가와 같은 추가 구현에 용이

 

 

Q1(드래그 하여 정답 확인)

1. (   단위 테스트  )는 애플리케이션의 개별 모듈을 독립적으로 테스트하는 방식이며, (  통합 테스트  )는 애플리케이션을 구성하는 다양한 모듈을 결합해 전체적인 로직이 의도한 대로 동작하는지 테스트하는 방식이다. 

2. 테스트 코드를 작성하는 방법에는 (  Given-When-Then  ) 패턴과 F.I.R.S.T 전략이 있다.

3. (  JUnit  )은 자바 언어에서 사용되는 대표적인 테스트 프레임워크로서 단위 테스트를 위한 도구를 제공한다. 가장 큰 특징은 어노테이션 기반의 테스트 방식을 지원한다는 점이다. 

4. JUnit의 생명주기와 관련된 어노테이션으로, 각 테스트 메서드가 실행되기 전에 동작하는 메서드를 정의하는 어노테이션은 (  @BeforeEach  )이다.

5. 테스트 과정에서 외부 요인이 있을 때 독립적인 테스트 코드를 작성하기 위해 (  Mock  )객체를 활용해야 한다.

6. 커버리지를 확인하기 위한 다양한 커버리지 도구 중 가장 보편적으로 사용되는 도구는 (  jaCoCo  )이다.

7. (  TDD, Test-Driven-Development, 테스트 주도 개발  )은 반복 테스트를 이용한 소프트웨어 개발 방법론으로서 테스트 코드를 먼저 작성한 후 테스트를 통과하는 코드를 작성하는 과정을 반복하는 소프트웨어 개발 방식이다.

 

Q2

public class TestLifeCycle {

    @BeforeAll
    static void beforeAll() {
        System.out.println("## BeforeAll Annotation 호출 ##");
        System.out.println();
    }

	// .. 생략 

    @Test
    // 테스트를 실행하지 않게 설정하는 어노테이션 작성
    void test3() {
        System.out.println("## test3 시작 ##");
        System.out.println();
    }

}

 

// 스프링의 모든 설정을 가져오고 빈 객체도 전체를 스캔하는 어노테이션 (@DataJpaTest X)
public class ProductRepositoryTest2 {

    @Autowired
    ProductRepository productRepository;

    @Test
    public void basicCRUDTest() {
		// .. 생략 
    }
}

 

 

출처: 스프링부트 핵심 가이드

SPRING #2

Editor : 노을

 

728x90

관련글 더보기