본문 바로가기
Java

Java - JUnit과 Mock

by sinabeuro 2021. 9. 27.
728x90

 

JUnit

Java 기반의 단위 테스트를 위한 프레임워크

Annotation 기반으로 테스트를 지원하며, Assert를 통하여, (예상, 실제)를 통해 검증합니다.

 

Junit을 사용하기 위해 2가지 의존성을 추가해야합니다.

mockito-core, mockito-junit-jupiter 

 

https://mvnrepository.com/search?q=mockito

// https://mvnrepository.com/artifact/org.mockito/mockito-core
testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.12.4'

// https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter
testImplementation group: 'org.mockito', name: 'mockito-junit-jupiter', version: '3.12.4'

 

test 디렉토리에 test class를 만드는 과정은 생략하겠습니다.

보통 본 디렉토리와 같은 구조로 test 디렉토리에 테스트하고 싶은 대상의 class를 만들면 됩니다.

인텔리제이에서는 test class를 자동으로 생성하는 기능이 있습니다. 

다른 IDE도 유사한 기능이 있을 것입니다. 

 

 

Assertions

단정 메소드(assert method)

 

    @Test
    public void dollerTest() {

        MarketServer marketServer = new MarketServer();
        System.out.println(marketServer.price());
        DollarCalculator dollarCalculator = new DollarCalculator(marketServer);
        Calculator calculator = new Calculator(dollarCalculator);

        Assertions.assertEquals(20, calculator.sum(10, 10));
    }

 

 

 

@SpringBootTest 와 @Import

test 패키지 내에서는 클래스의 인스턴스를 생성하지 않고 주입받아서 사용할 수 있습니다.

@SpringBootTest
@Import({MarketServer.class, DollarCalculator.class})
public class DollarCalculatorTest {

    @MockBean
    public MarketServer marketServer;

    @Autowired
    private DollarCalculator dollarCalculator;

    @Autowired
    private Calculator calculator;

    @Test
    public void dollarCalculatorTest() {

        System.out.println(marketServer.price());
        Mockito.when(marketServer.price()).thenReturn(3000);

        int sum = calculator.sum(10, 10);
        int minus = calculator.minus(10, 10);

        Assertions.assertEquals(60000, sum);
        Assertions.assertEquals(0, minus);
    }
}

@SpringBootTest 을 사용할 경우 class의 모든 빈이 등록됩니다.

@SpringBootTest 어노테이션은 통합테스트를 위해서 사용하는 어노테이션이라고 합니다.

단위 테스트는 @WebMvcTest, @DataJpaTest, @RestClientTest, @JsonTest 등을 사용한다고 합니다.

https://goddaehee.tistory.com/211

(테스트를 진행할 클래스를 먼저 빈으로 등록해주어야합니다. 클래스에 @Component 어노테이션을 붙여줍니다.)

 

그리고 @Import를 통해서 주입하고 싶은 클래스를 기술합니다.

참고로 기술하지 않아도 정상적으로 작동하기도 합니다.

클래스 내부의 또다른 클래스를 주입하는 경우가 아니라면 클래스 뎁스가 1인 경우에는 Import 없이도 사용가능한 것 같습니다.

그 후 @Autowired로 인스턴스를 생성해서 사용하면 됩니다.

그외에 @Import에서 빈으로 등록하지 않은 클래스를 사용할 경우 @MockBean으로 등록해서 사용하면 됩니다.

주의하실 점은 @MockBean으로 등록한 클래스의 내부 속성이 초기화됩니다.

그래서 Mockito.when(marketServer.price()).thenReturn(3000);와 같은 방식으로 thenReturn 메소드로 초기값을 새로 지정해줘야합니다.

 

 

@BeforeEach

@MockBean 등을 사용할 경우나 초기값 세팅이 필요한 경우에 @BeforeEach 어노테이션을 사용합니다.

@SpringBootTest
//@Import({ DollarCalculator.class})
public class DollarCalculatorTest {

    @MockBean
    public MarketServer marketServer;

    @Autowired
    private DollarCalculator dollarCalculator;

    @Autowired
    private Calculator calculator;
    
    @BeforeEach
    public void init(){
        Mockito.lenient().when(marketServer.price()).thenReturn(30000);
    }
    
    @Test
    public void dollerTest() {
        DollarCalculator dollarCalculator = new DollarCalculator(marketServer);
        Calculator calculator = new Calculator(dollarCalculator);
        Assertions.assertEquals(600000, calculator.sum(10, 10));
    }    
}

Mockito.lenient().when(marketServer.price()).thenReturn(30000); 와 같이 @BeforeEach 어노테이션으로 세팅하면 됩니다.

 

 

참고 @Mock vs @MockBean

@ExtendWith(MockitoExtension.class)

@ExtendWith(MockitoExtension.class)
@SpringBootTest
//@Import({ DollarCalculator.class})
public class DollarCalculatorTest {

    @Mock
    public MarketServer marketServer;
    
    @Autowired
    private DollarCalculator dollarCalculator;

    @Autowired
    private Calculator calculator;
    
    ...   
}

스프링은 @MockBean을 사용해서 인스턴스를 생성할 수 있지만, @MockBean을 이용하지 않고 빈없이 이용하기 위해서 @Mock을 사용하는 방법이 있습니다.

@Mock을 사용할 경우 반드시 @ExtendWith(MockitoExtension.class)을 같이 사용해주어야합니다.

 


 

컨트롤러 단위 테스트

@SpringBootTest을 사용하지않고 @WebMvcTest를 사용하여 컨트롤러마다 단위테스트를 할 수 있습니다.

 

get/delete 테스트

package com.example.junit.controller;

import com.example.junit.component.Calculator;
import com.example.junit.dto.Req;
import com.example.junit.dto.Res;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class CalculatorApiController {

    private final Calculator calculator;

    @GetMapping("/sum")
    public int sum(@RequestParam int x, @RequestParam int y) {
        return calculator.sum(x, y);
    }
}

간략하게 컨트롤러를 세팅합니다.

package com.example.junit.controller;

import com.example.junit.component.Calculator;
import com.example.junit.component.DollarCalculator;
import com.example.junit.component.MarketServer;
import com.example.junit.dto.Req;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@WebMvcTest(CalculatorApiController.class)      // @SpringBootTest 와 달리 필요한 것들만 load 시킨다.
@AutoConfigureWebMvc
@Import({Calculator.class, DollarCalculator.class})
class CalculatorApiControllerTest {

    @MockBean       // Import 대신에 @MockBean 으로 등록
    private MarketServer marketServer;

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    public void init(){
        Mockito.when(marketServer.price()).thenReturn(3000);
    }

    // get Test
    @Test
    void sum() throws Exception {
        // http://localhost:8080/api/sum
        mockMvc.perform(
            MockMvcRequestBuilders.get("http://localhost:8080/api/sum")
                .queryParam("x", "10")
                .queryParam("y", "10")
        ).andExpect(
                MockMvcResultMatchers.status().isOk()
        ).andExpect(
                MockMvcResultMatchers.content().string("60000")
        ).andDo(MockMvcResultHandlers.print());
    }
}

주의하실 점은 테스트하는 컨트롤러에서 사용하는 모든 클래스를 import 해야합니다. 

또한 import한 클래스 내부에서도 주입받아 사용하는 모든 클래스를 import 하거나 mockBean으로 등록해야합니다.

 

mockMvc 라이브러리 메소드를 통해서 컨트롤러 url를 호출하면 됩니다.

MockMvcRequestBuilders 메소드로 url를 호출하고,

andExpect 메소드로 response를 체크합니다.

andDo 메소드로 결과값을 출력합니다.

 

get으로 통신함으로, MockMvcRequestBuilders.get(URL) 메소드를 사용합니다.

이때 파라미터는 queryParam 메소드로 나열해서 보냅니다.

 

delete로 통신하는 경우도 get과 유사합니다. 

다만 MockMvcRequestBuilders.delete(URL) 메소드를 사용하면 됩니다.

 

get test 실행결과

 


 

post/put 메소드

package com.example.junit.controller;

import com.example.junit.component.Calculator;
import com.example.junit.dto.Req;
import com.example.junit.dto.Res;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class CalculatorApiController {

    private final Calculator calculator;

    @PostMapping("/minus")
    public Res minus(@RequestBody Req req) {
        int result = calculator.minus(req.getX(), req.getY());

        Res res = new Res();
        res.setResult(result);
        res.setResponse(new Res.Body());
        return res;
    }
}

 

package com.example.junit.controller;

import com.example.junit.component.Calculator;
import com.example.junit.component.DollarCalculator;
import com.example.junit.component.MarketServer;
import com.example.junit.dto.Req;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@WebMvcTest(CalculatorApiController.class)      // @SpringBootTest 와 달리 필요한 것들만 load 시킨다.
@AutoConfigureWebMvc
@Import({Calculator.class, DollarCalculator.class})
class CalculatorApiControllerTest {

    @MockBean       // Import 대신에 @MockBean 으로 등록
    private MarketServer marketServer;

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    public void init(){
        Mockito.when(marketServer.price()).thenReturn(3000);
    }

    // post Test
    @Test
    void minus() throws Exception {
        Req req = new Req();
        req.setX(10);
        req.setY(10);

        String json = new ObjectMapper().writeValueAsString(req);

        // http://localhost:8080/api/minus
        mockMvc.perform(
            MockMvcRequestBuilders.post("http://localhost:8080/api/minus")
                .contentType(MediaType.APPLICATION_JSON)
                .content(json)
        ).andExpect(
            MockMvcResultMatchers.status().isOk()
        ).andExpect(
            MockMvcResultMatchers.jsonPath("$.result").value(0)
        ).andExpect(
            MockMvcResultMatchers.jsonPath("$.response.resultCode").value("OK")    // 뎁스가 있는 결과값
        )
        .andDo(MockMvcResultHandlers.print());
    }
}

post 방식의 test 역시 mockMvc 라이브러리 메소드를 통해서 컨트롤러 url를 호출하면 됩니다.

MockMvcRequestBuilders.post(URL) 메소드로 테스트 통신하기 전에 json 형태로 파라미터를 만들어야합니다.

Req 인스턴스를 생성하여, 파라미터를 세팅하여 string 형태의 json을 만든 후 컨트롤러 url 파라미터로 넘겨줍니다.

MockMvcRequestBuilders.post(URL).contentType()을 json으로 지정해주고 content 메소드를 통해서 파라미터를 넘깁니다.

 

결과값(response) 체크방식은 get test와는 많이 다릅니다.

MockMvcResultMatchers.jsonPath(...).value(...) 메소드를 이용하여 resposne를 체크할 수 있습니다.

jsonPath에 response 경로를 입력해주어야하는데, 이 부분의 문법을 주의해서 값을 체크하시면 됩니다.

아래의 response 결과 이미지와 jsonPath 문법을 비교해서 숙지하시길 바랍니다.

2뎁스 이상의 response 값은 MockMvcResultMatchers.jsonPath("$.response.resultCode").value("OK") 예시를 참조하시 됩니다.

post test 실행결과

 


 

참고 - 테스트 커버리지 jacoco

 

jacoco 라이브러를 사용하면 junit으로 테스트한 결과를 report 형식으로 받아볼 수 있습니다.

 

plugins {
    id 'org.springframework.boot' version '2.5.5'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
    id 'jacoco'
}

gradle 기준으로 plugins에 id 'jacoco' 를 추가하면 jacoco 이용할 수 있습니다.

 

테스트 시 build 디렉토리 내에 report 디렉토리가 생성된 것을 확인할 수 있습니다.

reports -> tests -> test -> packages -> index.html 

해당 경로로 접근하면 report를 볼 수 있습니다.

브라우저에 해당 html을 드래그하셔서 가시적으로 화면에 출력할 수 있습니다.

 

 

 

참고 - 간편한? 테스트 작성 소개

MockMvcResultHandlers, MockMvcResultMatchers 메소드 없이 간략하게 junit5로 테스트 코드를 작성할 수 있습니다.

하지만 static 형식으로 print(), status(), content() 등의 메소드를 사용해야 합니다.

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.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


@WebMvcTest
class HelloWorldControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    void helloworld() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/hello-world"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().string("hello-world"));
    }
}

@SpringBootTest 어노테이션 대신 @WebMvcTest를 사용하여 테스트 코드를 작성할 수 있습니다.

 

 

 

 

https://beomseok95.tistory.com/205

 

Junit - Assert 메소드

단정 메소드(assert method) ·         JUnit에서 가장 많이 이용되는 단정(assert) 메소드입니다. ·         단정 메서드로 테스트 케이스의 수행 결과를 판별합니다. 메소드 설..

beomseok95.tistory.com

 

728x90

'Java' 카테고리의 다른 글

Jave - Thread 개괄  (0) 2022.05.03
Java - 객체 지향 설계 원칙 SOLID  (0) 2022.04.09
Java - ObjectMapper의 JsonNode, ObjectNode, ArrayNode  (0) 2021.09.07
java - RequestHeader  (0) 2021.08.31
java - ObjectMapper  (0) 2021.08.29

댓글