본문 바로가기
개발 관련 공부/스프링부트 핵심 가이드

07 테스트 코드 작성하기(2)

by 슴새 2023. 12. 24.
반응형

 

11월말..12월초 너무 바빴다..ㅠㅠ

오랜만에 작성하는 스프링부트 시리즈

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

컨트롤러 객체의 테스트

예를들어 productController의 테스트를 하고싶다...

그러면 test 폴더 아래에 ProductControllerTest 를 만들어야 한다.

 

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

 

Mock 객체를 사용하여 테스트를 수행한다. Mock는 가짜 객체이기 때문에 실제 db 등에 영향을 주지 않게 된다.

 

각 어노테이션에 대해 설명하자면..

 

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

@SpringBootTest보다 가볍게 테스트할때 사용됨.

해당 클래스만 로드해 테스트를 수행한다.

대상클래스를 추가하지 않으면 컨트롤러 관련 빈 객체가 모두 로드됨.

 

이걸 사용한 테스트를 슬라이스 테스트라고 부른다.

단위 테스트를 수행하기 위해서는 모든 외부 요인을 차단하고 테스트를 진행해야 하지만 컨트롤러는 웹과 맞닿은 레이어로서 외부 요인을 차단하면 의미가 없기 때문에 슬라이스 테스트로 많이 진행한다.

 

@MockBean

가짜 객체를 생성해서 주입하는 역할. 

가짜기 때문에..개발자가 Mockito의 given 메서드를 통해 동작을 정의해야 한다.

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

123L을 인자로 getProduct를 호출하면 이러한 결과가 나와야 한다~ 뭐 이런뜻이다.

 

@Test

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

 

@DisplayName

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

 

테스트하는 과정을 좀 더 자세히 살펴보면...

		// 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);

perform 메서드를 통해 가상의 mvc 환경에서 서버로 url 요청을 보내는 것처럼 동작하게 할 수 있다. 

이후 andExpect를 통해 결과값 검증을 할 수 있다. (리턴된 json에 key가 number,name..인게 있는지 확인)

 

이후 verify를 통해 해당 객체의 메소드가 실행되었는지 체크한다.(given과 같은 역할...그렇다는건 올바른 값이 왔는지 확인도 같이 한다는 뜻이겠지?)

 

다음으로는 saveProduct에 대한 테스트도 해보자.

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

 

post이므로 pom.xml에 Gson에 대한 의존성을 추가해준다.

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

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

그러면 이런식으로 객체를 json 타입으로 만들어 테스트에 활용할 수 있다.

@RequestBody는 content 메서드를 통해 테스트한다.

나머지는 get과 비슷하다.

 

서비스객체의 테스트

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
        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);
    }
   
    // 예제 7.12
    @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());
    }
}

 

마찬가지로 ProductServiceTest.java를 생성한다.

얘는 웹과 맞닿아 있지 않으므로 외부 요인을 배제한다. @SpringBootTest, @WebMvcTest 등을 선언하지 않는단 소리.

 

  private ProductRepository productRepository = Mockito.mock(ProductRepository.class);
  private ProductServiceImpl productService;
  
    @BeforeEach
    public void setUpTest() {
        productService = new ProductServiceImpl(productRepository);
    }

@MockBean을 사용한 컨트롤러 실습과는 좀 다른 방법으로 주입을 받아보았다.

@MockBean은 스프링에 Mock 객체를 등록해서 주입받는 형식이며

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

기능에 차이는 없다.

 

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

    @MockBean
    ProductRepository productRepository;

    @Autowired
    ProductService productService;
  ...
  
 }

만약 서비스에서 @MockBean을 사용하려면

스프링컨텍스트가 알아보도록 클래스 이름위에 어노테이션을 붙여야한다.

 

이제 테스트가 어떻게 동작하는지 보도록 하자.

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

복습하자면 given-when-then은

Given: 테스트에 필요한 환경 설정 (ex: 나는 네이버 100만원어치 주식을 가지고 있다.)

When: 테스트의 목적을 보여줌 (ex: 나는 네이버 20만원어치 주식 20을 팔도록 요청한다.)

Then: 테스트의 결과를 검증 (ex: 내가 들고있는 네이버 주식은 80만원이어야 한다.)

 

이런식이다.

given-when-then 패턴에 맞춰...

given: 지금 환경은 특정 product 객체에 대해 조회 요청이 발생한 상황이고,

when: 인자를 받아 구체적인 조회 메서드를 호출했을때, 

 

then: 올바른 값인지 검증

 

진짜 어렵네....실제 동작코드보다 테스트코드가 더 복잡한 것 같다;

 

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

save도 마찬가지이다. 

 

given: 지금 환경은 any product 객체에 대해 조회 요청이 발생한 상황이고,

when: 구체적인 save 메서드를 호출했을때, 

then: 올바른 값인지 검증

 

 

 

 

반응형

댓글