SpringBoot TestCode (3) - JUnit과 Mock 객체를 사용한 테스트 코드

Yu Seong Kim·2023년 12월 18일
0

SpringBoot

목록 보기
3/29

📌MockMVC란?

MockMvc는 웹 어플리케이션을 애플리케이션 서버에 배포하지 않고 테스트용 MVC환경을 만들어 요청 및 전송, 응답기능을 제공해주는 유틸리티 클래스다.

컨트롤러 테스트를 하고 싶을 때, 실제 서버가 아닌 테스트용으로 시뮬레이션 하여 MVC가 되도록 도와주는 클래스 이다.


📌MockMVC 사용법

package com.springboot.junittest.test.controller;

import com.springboot.junittest.controller.ProductController;
import com.springboot.junittest.data.dto.ProductResponseDto;
import com.springboot.junittest.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.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.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 Productcontroller {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    ProductServiceImpl productService;

    // http://localhost:8080/api/v1/product-api/product/{productId} 테스트 해볼것

    @Test
    @DisplayName("MockMvc를 통한 Product 데이터 가져오기 테스트")
    void getProductTest() throws Exception{
        given(productService.getProduct(123L)).willReturn(
                new ProductResponseDto(123L,"pencil",500,2000));
        String productId = "123";
        mockMvc.perform(
                get("/product?number="+productId))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.number").exists())
                .andExpect(jsonPath("$.price").exists())
                .andExpect(jsonPath("$.stock").exists())
                .andDo(print());

        verify(productService).getProduct(123L);

    }

}

@WebMvcTest(테스트 대상 클래스.class)

웹에서 사용되는 요청과 응답에 대한 테스트를 수행할 수 있다. 대상 클래스만 로드해 테스트를 수행,

만약 대상 클래스를 추가하지 않으면 @Controller, @RestController, @ContollerAdvice 등의 컨트롤러 관련 빈 객체가 모두 로드된다

@SpringBootTest보다 가볍게 테스트하기 위해 사용

@MockBean

MockBean은 실제 빈 객체가 아닌 Mock(가짜) 객체를 생성해서 주입하는 역할을 수행한다.

MockBean 이 선언된 객체는 실제 객체가 아니기 때문에 실제 행위를 수행하지 않는다.

그렇기 때문에 해당 객체는 개발자가 Mockito의 given() 메서드를 통해 동작을 정의해야 한다.

@Test

테스트 코드가 포함돼 있다고 선언하는 어노테이션, JUnit Jupiter 에서는 이 어노테이션을 감지해서 테스트 계획이 포함시킨다.

@DisplayName

테스트 메서드의 이름이 복작해서 가독성이 떨어질 경우 이 어노테이션을 통해 테스트에 대한 표현을 정의할 수 있다.

-일반적으로 @WebMvcTest 어노테이션을 사용한 테스트는 슬라이스 테스트(Slice test) 라고한다

레이어 별로 나누어서 테스트를 하는데, 컨트롤러는 Web과 맞닿아 있는 레이어 이기 때문에

외부요인을 차단하고 테스트하면 의미가 없다. -


📌서비스 객체의 단위테스트

package com.springboot.junittest.service;

import com.springboot.junittest.data.dto.ProductDto;
import com.springboot.junittest.data.dto.ProductResponseDto;
import com.springboot.junittest.data.entity.Product;
import com.springboot.junittest.data.repository.ProductRepository;
import com.springboot.junittest.service.Impl.ProductServiceImpl;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.Optional;

import static org.mockito.AdditionalAnswers.returnsFirstArg;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;

public class ProductServiceTest {
    private ProductRepository productRepository
            = Mockito.mock(ProductRepository.class);
    private ProductServiceImpl productService;

    @BeforeEach
    public void setUpTest(){
        productService = new ProductServiceImpl(productRepository);
    }

    @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(조건)일때 결과는 thenReturn(결과)
        //Optional -> null일 수도 있는 객체 but .of 를 붙히면 null 없는 것.

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

        //then
        Assertions.assertEquals(productResponseDto.getNumber(),givenProduct.getNumber());
//      //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);

    }
    @BeforeEach
     void saveProductTestBeforeEach(){
        System.out.println("saveProductTest() 호출");
    }
    @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());
    }
    @AfterEach
    void saveProductTestAfterEach(){
        System.out.println("saveProductTest() 호출 종료");
    }
}

단위테스트를 위해서 외부요인을 모두 제거 → WebMvcTest 등의 어노테이션 x
기존- Mockito로 Mock객체를 SpringBean에 등록x, 객체를 직접 초기화 하여 사용


📌레포지토리 객체의 단위테스트

개발자가 구현하는 레이엉 중 데이터베이스와 가깝다.

리포지토리 객체의 테스트 코드를 작성할 때 고려할점

1, findById(), save() 같은 기본 메서드에 대한 테스트는 큰 의미가 없다
-> 리포지토리의 기본 메서드는 테스트 검증을 마치고 제공된 것이기 때문

2, 데이터베이스의 연동 여부는 테스트 시 고려해 볼 사항이다
굳이 따지면 데이터베이스는 외부 요인에 속한다
만약 단위 테스트를 고려한다면 데이터베이스를 제외할 수 있고, 테스트 용으로 다른 데이터베이스를 사용하는 경우도 있다
-> 데이터베이스를 사용한 테스트는 테스트 과정에서 데이터베이스에 테스트 데이터가 적재되기 때문
※적재된 데이터를 그냥 둘 수 없으므로 제거하는 코드까지 작성

3, 데이터베이스를 제외한 테스트 상황을 가정해서 테스트 데이터베이스로 H2 DB 를 사용, 테스트 할때는 마리아DB를 사용
JpaRepository에서 제공하는 기본 메서드 사용

H2 DB를 사용한 테스트 코드를 작성

<dependencies>
    ...생략...
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>test</scope>
    </dependency>
    ...생략...
</dependencies>

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

test/com.springboot.test -> data/repository 패키지를 생성 -> ProductRepositoryTestByH2.java 파일을 생성

package com.springboot.test.data.repository;

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

import com.springboot.test.data.entity.Product;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;


@DataJpaTest
public class ProductRepositoryTestByH2 {

    @Autowired
    private ProductRepository productRepository;

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

        
        Product savedProduct = productRepository.save(product);

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

※@DataJpaTest 어노테이션을 사용하고 있다

@DataJpaTest는 다음과 같은 기능을 제공

1, JPA와 관련된 설정만 로드해서 테스트를 진행
기본적으로 @Transactional 어노테이션을 포함하고 있어 테스트 코드가 종료되면 자동으로 데이터베이스의 롤백이 진행된다

2, 기본 값으로 임베디드 데이터베이스를 사용

다른 데이터베이스를 사용하라면 별도의 설정을 거쳐 사용 가능하다

이후 정상적인 테스트가 이뤄졌는지 체크

save() 메서드의 리턴 객체와 Given에서 생성한 엔티티 객체의 값이 일치하는지 assertEquals() 메서드를 통해 검증

package com.springboot.test.data.repository;

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

import com.springboot.test.data.entity.Product;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

//@TestPropertySource("classpath:application-test.properties")
@DataJpaTest
public class ProductRepositoryTestByH2 {

   @Autowired
   private ProductRepository productRepository;


@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());
   }
}

마리아DB에서 테스트하기 위해서는 별도의 설정이 필요

같은 패키지 경로

test/com.springboot.test -> data/repository 패키지 - ProductRepositoryTest.java 파일을 생성하고 아래와 같이 코드를 작성

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

    @Autowired
    private ProductRepository productRepository;

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

        Product savedProduct = productRepository.save(product);

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

}

replace 요소는 @AutoConfigureTestDatabase 어노테이션의 값을 조정하는 작업을 수행한다

replace 속성의 기본값은 Replcae.ANY 이며, 이 경우 임베디드 메모리 데이터베이스를 사용한다

이 속성값을 Replace.NONE 으로 변경하면 애플리케이션에서 실제로 사용하는 데이터베이스로 테스트 가능

여기까지 하면 @DataJpaTest를 사용하지 않고 @SpringBootTest 어노테이션 으로도 테스트 가능

같은 패키지 경로에 ProductRepositoryTest2.java 파일을 생성

@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 selectProduct = productRepository.findById(savedProduct.getNumber())
                .orElseThrow(RuntimeException::new);

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

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

        foundProduct.setName("장난감");

        Product updatedProduct = productRepository.save(foundProduct);

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

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

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

}

기본적인 메서드를 테스트 하기 때문에 Given 구문을 한 번만 사용해 전체 테스트에 활용했다

@SpringBootTest 어노테이션을 활용하면 테스트가 가능한가?

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

그러나 시간이 매우 오래 걸려서 효율적이지 않다

profile
Development Record Page

0개의 댓글