網頁

2020/4/10

Spring Boot + JUnit 5 使用 MockMvc 測試 RestController API

Sprinb Boot 使用JUnit 5搭配Spring MockMvc測試RestController API範例。


環境如下:

  • Java 1.8
  • IntelliJ IDEA 2019.2.1(Community Edition)
  • Spring Boot 2.2.1.RELEASE
  • Maven

Spring Boot專案的建立請參考:


Spring Boot 2.2版本以後JUnit版本預設為5,2.2 以前的JUnit版本為4需調整依賴設定


本範例專案的pom.xml內容。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.abc</groupId>
    <artifactId>demo1</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo1</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <compilerVersion>1.8</compilerVersion>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

建立一支被測試的Controller類別DemoController,內含一支被測試的方法hello()如下。

DemoController

package com.abc.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

    @GetMapping(value = "/hello")
    public String hello() {
        String hello = "hello";
        System.out.println(hello);
        return hello;
    }

}

建立一支負責測試DemoController的測試用類別DemoControllerTests如下,類別名稱前加上@WebMvcTest。利用MockMvc發出GET請求給/hello API,也就是DemoController.hello()

DemoControllerTests

package com.abc.demo.controller;

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 static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(DemoController.class)
class DemoControllerTests {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void hello_ReturnHello() throws Exception {

        mockMvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().string("hello"));

    }
}

設定完以上後執行DemoControllerTests.hello_ResturnHello()來測試/hello API。執行時會啟動Spring Boot應用程式,結果印出如下。測試結果為通過。

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.1.RELEASE)

2020-01-07 12:41:46.311  INFO 88510 --- [           main] c.a.demo.controller.DemoControllerTests  : Starting DemoControllerTests on ...
2020-01-07 12:41:46.329  INFO 88510 --- [           main] c.a.demo.controller.DemoControllerTests  : No active profile set, falling back to default profiles: default
2020-01-07 12:41:47.468  INFO 88510 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-01-07 12:41:47.743  INFO 88510 --- [           main] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2020-01-07 12:41:47.743  INFO 88510 --- [           main] o.s.t.web.servlet.TestDispatcherServlet  : Initializing Servlet ''
2020-01-07 12:41:47.774  INFO 88510 --- [           main] o.s.t.web.servlet.TestDispatcherServlet  : Completed initialization in 31 ms
2020-01-07 12:41:47.807  INFO 88510 --- [           main] c.a.demo.controller.DemoControllerTests  : Started DemoControllerTests in 1.817 seconds (JVM running for 3.42)

hello

2020-01-07 12:41:48.176  INFO 88510 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'

Process finished with exit code 0

實務上Controller通常會依賴於Service類來取得資料庫內容之類的,而@WebMvcTest注釋預設只會自動配置Spring MVC相關元件,例如@Controller@ControllerAdvice@JsonComponentFilterWebMvcConfigurer及Spring Security,MockMvc等;其他非屬Spring MVC的元件如@Component@Service@Repository等並不會被自動配置。

因此使用@WebMvcTest進行Controller的單元測試時,通常會搭配@MockBean來mock依賴的非Spring MVC Bean。

例如新增一個Service類DemoService內容如下,類別名稱前加上@Service

DemoService

package com.abc.demo.service;

import org.springframework.stereotype.Service;

@Service
public class DemoService {

    public String getHello() {
        return "hello";
    }
}

DemoController.hello()回傳的內容改為依賴的DemoService.getHello()提供。

DemoController

package com.abc.demo.controller;

import com.abc.demo.service.DemoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

    @Autowired
    private DemoService demoService;

    @GetMapping(value = "/hello")
    public String hello() {
        String hello = demoService.getHello();
        System.out.println(hello); // hello
        return hello;
    }
}

DemoControllerTests修改如下,以@MockBeanDemoService做成mock,並利用Mockito對mock bean做stubbing返回指定的值。

DemoControllerTests

package com.abc.demo.controller;

import com.abc.demo.service.DemoService;
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.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(DemoController.class)
class DemoControllerTests {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private DemoService demoService;

    @Test
    void hello_ReturnHello() throws Exception {

        Mockito.when(demoService.getHello()).thenReturn("hello"); // Mockito stubbing

        mockMvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().string("hello"));

    }
}

如果沒對依賴的DemoService進行mock,則執行時會出現NoSuchBeanDefinitionException錯誤,原因如上所述,@WebMvcTest測試並不會自動配置非Spring MVC元件如@Service


如果希望測試時自動配置全部Bean(例如整合測試),而不僅限於Spring MVC元件,則測試類可改用@AutoConfigureMockMvc搭配@SpringBootTest來執行測試。

例如下面把測試類DemoControllerTests中原本的@WebMvcTest替換成@AutoConfigureMockMvc搭配@SpringBootTest,因此在執行測試時也會自動配置測試對象依賴的非Spring MVC元件,也就是DemoService。所以把mock bean及Mockito stubbing註解掉後,則測試/hello接點過程中呼叫的DemoService.getHello()是真實物件回傳的結果。

DemoControllerTests

package com.abc.demo.controller;

import com.abc.demo.service.DemoService;
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.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@AutoConfigureMockMvc // <--
@SpringBootTest        // <--
class DemoControllerTests {

    @Autowired
    private MockMvc mockMvc;

//    @MockBean
//    private DemoService demoService;

    @Test
    void hello_ReturnHello() throws Exception {

//        Mockito.when(demoService.getHello()).thenReturn("hello");

        mockMvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().string("hello"));

    }
}

參考:

沒有留言:

張貼留言