欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringBoot 應(yīng)用程序測(cè)試實(shí)現(xiàn)方案

 更新時(shí)間:2021年11月04日 09:23:36   作者:小小工匠  
這篇文章主要介紹了SpringBoot 應(yīng)用程序測(cè)試實(shí)現(xiàn)方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教

Pre

本篇博文我們開(kāi)始梳理下Spring 提供的測(cè)試解決方案。

對(duì)于 Web 應(yīng)用程序而言, 一個(gè)應(yīng)用程序中涉及數(shù)據(jù)層、服務(wù)層、Web 層,以及各種外部服務(wù)之間的交互關(guān)系時(shí),我們除了對(duì)各層組件的單元測(cè)試之外,還需要充分引入集成測(cè)試保證服務(wù)的正確性和穩(wěn)定性。

Spring Boot 中的測(cè)試解決方案

和 Spring Boot 1.x 版本一樣,Spring Boot 2.x 也提供了一個(gè)用于測(cè)試的 spring-boot-starter-test 組件。

在 Spring Boot 中,集成該組件的方法是在 pom 文件中添加如下所示依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
        
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-launcher</artifactId>
    <scope>test</scope>
</dependency>

其中,最后一個(gè)依賴用于導(dǎo)入與 JUnit 相關(guān)的功能組件。

然后,通過(guò) Maven 查看 spring-boot-starter-test 組件的依賴關(guān)系,我們可以得到如下所示的組件依賴圖:

在這里插入圖片描述

從上圖中可以看到,在代碼工程的構(gòu)建路徑中,我們引入了一系列組件初始化測(cè)試環(huán)境。比如 JUnit、JSON Path、AssertJ、Mockito、Hamcrest 等

  • JUnit:JUnit 是一款非常流行的基于 Java 語(yǔ)言的單元測(cè)試框架
  • JSON Path:類似于 XPath 在 XML 文檔中的定位,JSON Path 表達(dá)式通常用來(lái)檢索路徑或設(shè)置 JSON 文件中的數(shù)據(jù)。
  • AssertJ:AssertJ 是一款強(qiáng)大的流式斷言工具,它需要遵守 3A 核心原則,即 Arrange(初始化測(cè)試對(duì)象或準(zhǔn)備測(cè)試數(shù)據(jù))——> Actor(調(diào)用被測(cè)方法)——>Assert(執(zhí)行斷言)。
  • Mockito:Mockito 是 Java 世界中一款流行的 Mock 測(cè)試框架,它主要使用簡(jiǎn)潔的 API 實(shí)現(xiàn)模擬操作。在實(shí)施集成測(cè)試時(shí),我們將大量使用到這個(gè)框架。
  • Hamcrest:Hamcrest 提供了一套匹配器(Matcher),其中每個(gè)匹配器的設(shè)計(jì)用于執(zhí)行特定的比較操作。
  • JSONassert:JSONassert 是一款專門針對(duì) JSON 提供的斷言框架。
  • Spring Test & Spring Boot Test:為 Spring 和 Spring Boot 框架提供的測(cè)試工具。

以上組件的依賴關(guān)系都是自動(dòng)導(dǎo)入, 無(wú)須做任何變動(dòng)。

測(cè)試 Spring Boot 應(yīng)用程序

接下來(lái),我們將初始化 Spring Boot 應(yīng)用程序的測(cè)試環(huán)境,并介紹如何在單個(gè)服務(wù)內(nèi)部完成單元測(cè)試的方法和技巧。

導(dǎo)入 spring-boot-starter-test 依賴后,我們就可以使用它提供的各項(xiàng)功能應(yīng)對(duì)復(fù)雜的測(cè)試場(chǎng)景了。

初始化測(cè)試環(huán)境

對(duì)于 Spring Boot 應(yīng)用程序而言,我們知道其 Bootstrap 類中的 main() 入口將通過(guò) SpringApplication.run() 方法啟動(dòng) Spring 容器.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

針對(duì)上述 Bootstrap 類,我們可以通過(guò)編寫(xiě)測(cè)試用例的方式,驗(yàn)證 Spring 容器能否正常啟動(dòng)。

在這里插入圖片描述

基于 Maven 的默認(rèn)風(fēng)格,我們將在 src/test/javasrc/test/resources 包下添加各種測(cè)試用例代碼和配置文件。

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringRunner;
 
@SpringBootTest
@RunWith(SpringRunner.class)
public class ApplicationContextTests {
 
    @Autowired
    private ApplicationContext applicationContext;
 
    @Test
    public void testContextLoads() throws Throwable {
        Assert.assertNotNull(this.applicationContext);
    }
}

該用例對(duì) Spring 中的 ApplicationContext 作了簡(jiǎn)單非空驗(yàn)證。

執(zhí)行該測(cè)試用例后,從輸出的控制臺(tái)信息中,我們可以看到 Spring Boot 應(yīng)用程序被正常啟動(dòng),同時(shí)測(cè)試用例本身也會(huì)給出執(zhí)行成功的提示。

上述測(cè)試用例雖然簡(jiǎn)單,但是已經(jīng)包含了測(cè)試 Spring Boot 應(yīng)用程序的基本代碼框架。其中,最重要的是 ApplicationContextTests 類上的 @SpringBootTest 和 @RunWith 注解,對(duì)于 Spring Boot 應(yīng)用程序而言,這兩個(gè)注解構(gòu)成了一套完成的測(cè)試方案。

接下來(lái)我們對(duì)這兩個(gè)注解進(jìn)行詳細(xì)展開(kāi)。

@SpringBootTest

因?yàn)?SpringBoot 程序的入口是 Bootstrap 類,所以 SpringBoot 專門提供了一個(gè) @SpringBootTest 注解測(cè)試 Bootstrap 類。同時(shí) @SpringBootTest 注解也可以引用 Bootstrap 類的配置,因?yàn)樗信渲枚紩?huì)通過(guò) Bootstrap 類去加載。

在上面的例子中,我們是通過(guò)直接使用 @SpringBootTest 注解提供的默認(rèn)功能對(duì)作為 Bootstrap 類的 Application 類進(jìn)行測(cè)試。

而更常見(jiàn)的做法是在 @SpringBootTest 注解中指定該 Bootstrap 類,并設(shè)置測(cè)試的 Web 環(huán)境,如下代碼所示。

@SpringBootTest(classes = CustomerApplication.class, 
	webEnvironment = SpringBootTest.WebEnvironment.MOCK)

在以上代碼中,@SpringBootTest 注解中的 webEnvironment 可以有四個(gè)選項(xiàng),分別是 MOCK、RANDOM_PORT、DEFINED_PORT 和 NONE。

@SpringBootTest - webEnvironment

  • MOCK:加載 WebApplicationContext 并提供一個(gè) Mock 的 Servlet 環(huán)境,此時(shí)內(nèi)置的 Servlet 容器并沒(méi)有正式啟動(dòng)。
  • RANDOM_PORT:加載 EmbeddedWebApplicationContext 并提供一個(gè)真實(shí)的 Servlet 環(huán)境,然后使用一個(gè)隨機(jī)端口啟動(dòng)內(nèi)置容器。
  • DEFINED_PORT:這個(gè)配置也是通過(guò)加載 EmbeddedWebApplicationContext 提供一個(gè)真實(shí)的 Servlet 環(huán)境,但使用的是默認(rèn)端口,如果沒(méi)有配置端口就使用 8080。
  • NONE:加載 ApplicationContext 但并不提供任何真實(shí)的 Servlet 環(huán)境。

在 Spring Boot 中,@SpringBootTest 注解主要用于測(cè)試基于自動(dòng)配置的 ApplicationContext,它允許我們?cè)O(shè)置測(cè)試上下文中的 Servlet 環(huán)境。

在多數(shù)場(chǎng)景下,一個(gè)真實(shí)的 Servlet 環(huán)境對(duì)于測(cè)試而言過(guò)于重量級(jí),通過(guò) MOCK 環(huán)境則可以緩解這種環(huán)境約束所帶來(lái)的困擾

@RunWith 注解與 SpringRunner

在上面的示例中,我們還看到一個(gè)由 JUnit 框架提供的 @RunWith 注解,它用于設(shè)置測(cè)試運(yùn)行器。例如,我們可以通過(guò) @RunWith(SpringJUnit4ClassRunner.class) 讓測(cè)試運(yùn)行于 Spring 測(cè)試環(huán)境。

雖然這我們指定的是 SpringRunner.class,實(shí)際上,SpringRunner 就是 SpringJUnit4ClassRunner 的簡(jiǎn)化,它允許 JUnit 和 Spring TestContext 整合運(yùn)行,而 Spring TestContext 則提供了用于測(cè)試 Spring 應(yīng)用程序的各項(xiàng)通用的支持功能。

執(zhí)行測(cè)試用例

接下來(lái)我們將通過(guò)代碼示例回顧如何使用 JUnit 框架執(zhí)行單元測(cè)試的過(guò)程和實(shí)踐,同時(shí)提供驗(yàn)證異常和驗(yàn)證正確性的測(cè)試方法。

單元測(cè)試的應(yīng)用場(chǎng)景是一個(gè)獨(dú)立的類,如下所示的 CustomerTicket 類就是一個(gè)非常典型的獨(dú)立類:

public class CustomTicket {
    private Long id;
    private Long accountId;    
    private String orderNumber;
    private String description;
    private Date createTime;
    public CustomTicket (Long accountId, String orderNumber) {
        super();
        Assert.notNull(accountId, "Account Id must not be null");
        Assert.notNull(orderNumber, "Order Number must not be null");
        Assert.isTrue(orderNumber.length() == 10, "Order Number must be exactly 10 characters");
        this.accountId = accountId;
        this.orderNumber = orderNumber;
    }
       …
}

我們可以看到,該類對(duì)CustomTicket 做了封裝,并在其構(gòu)造函數(shù)中添加了校驗(yàn)機(jī)制。

下面我們先來(lái)看看如何對(duì)正常場(chǎng)景進(jìn)行測(cè)試。

例如 ArtisanTicket 中orderNumber 的長(zhǎng)度問(wèn)題,我們可以使用如下測(cè)試用例,通過(guò)在構(gòu)造函數(shù)中傳入字符串來(lái)驗(yàn)證規(guī)則的正確性:

@RunWith(SpringRunner.class)
public class CustomerTicketTests {
    private static final String ORDER_NUMBER = "Order00001";
    @Test
    public void testOrderNumberIsExactly10Chars() throws Exception {
        CustomerTicket customerTicket = new CustomerTicket(100L, ORDER_NUMBER);
        assertThat(customerTicket.getOrderNumber().toString()).isEqualTo(ORDER_NUMBER);
    }
}

使用 @DataJpaTest 注解測(cè)試數(shù)據(jù)訪問(wèn)組件

數(shù)據(jù)需要持久化,接下來(lái)我們將從數(shù)據(jù)持久化的角度出發(fā),討論如何對(duì) Repository 層進(jìn)行測(cè)試的方法。

首先,我們討論一下使用關(guān)系型數(shù)據(jù)庫(kù)的場(chǎng)景,并引入針對(duì) JPA 數(shù)據(jù)訪問(wèn)技術(shù)的 @DataJpaTest 注解

@DataJpaTest 注解會(huì)自動(dòng)注入各種 Repository 類,并初始化一個(gè)內(nèi)存數(shù)據(jù)庫(kù)和及訪問(wèn)該數(shù)據(jù)庫(kù)的數(shù)據(jù)源。在測(cè)試場(chǎng)景下,一般我們可以使用 H2 作為內(nèi)存數(shù)據(jù)庫(kù),并通過(guò) MySQL 實(shí)現(xiàn)數(shù)據(jù)持久化,因此我們需要引入以下所示的 Maven 依賴:

<dependency>
       <groupId>com.h2database</groupId>
       <artifactId>h2</artifactId>
</dependency>
<dependency>
       <groupId>mysql</groupId>
       <artifactId>mysql-connector-java</artifactId>
       <scope>runtime</scope>
</dependency>

另一方面,我們需要準(zhǔn)備數(shù)據(jù)庫(kù) DDL 用于初始化數(shù)據(jù)庫(kù)表,并提供 DML 腳本完成數(shù)據(jù)初始化。其中,schema-mysql.sql 和 data-h2.sql 腳本分別充當(dāng)了 DDL 和 DML 的作用。

在 customer-service 的 schema-mysql.sql 中包含了 CUSTOMER 表的創(chuàng)建語(yǔ)句,如下代碼所示:

DROP TABLE IF EXISTS `customerticket`;
create table `customerticket` (
	   `id` bigint(20) NOT NULL AUTO_INCREMENT,
	   `account_id` bigint(20) not null,
	   `order_number` varchar(50) not null,
	   `description` varchar(100) not null,
	    `create_time` timestamp not null DEFAULT CURRENT_TIMESTAMP,
	   PRIMARY KEY (`id`)
);

而在 data-h2.sql 中,我們插入了一條測(cè)試需要使用的數(shù)據(jù),具體的初始化數(shù)據(jù)過(guò)程如下代碼所示:

INSERT INTO customerticket (`account_id`, `order_number`,`description`) values (1, 'Order00001', ' DemoCustomerTicket1');

接下來(lái)是提供具體的 Repository 接口,我們先通過(guò)如下所示代碼回顧一下 CustomerRepository 接口的定義。

public interface CustomerTicketRepository extends JpaRepository<CustomerTicket, Long> {
    List<CustomerTicket> getCustomerTicketByOrderNumber(String orderNumber);
}

這里存在一個(gè)方法名衍生查詢 getCustomerTicketByOrderNumber,它會(huì)根據(jù) OrderNumber 獲取 CustomerTicket。

基于上述 CustomerRepository,我們可以編寫(xiě)如下所示的測(cè)試用例:

@RunWith(SpringRunner.class)
@DataJpaTest
public class CustomerRepositoryTest {
    @Autowired
    private TestEntityManager entityManager;
 
    @Autowired
    private CustomerTicketRepository customerTicketRepository;
 
    @Test
    public void testFindCustomerTicketById() throws Exception {             
        this.entityManager.persist(new CustomerTicket(1L, "Order00001", "DemoCustomerTicket1", new Date()));
        
        CustomerTicket customerTicket = this.customerTicketRepository.getOne(1L);
        assertThat(customerTicket).isNotNull();
        assertThat(customerTicket.getId()).isEqualTo(1L);
    }
        
    @Test
    public void testFindCustomerTicketByOrderNumber() throws Exception {    
        String orderNumber = "Order00001";
        
        this.entityManager.persist(new CustomerTicket(1L, orderNumber, "DemoCustomerTicket1", new Date()));
        this.entityManager.persist(new CustomerTicket(2L, orderNumber, "DemoCustomerTicket2", new Date()));
        
        List<CustomerTicket> customerTickets = this.customerTicketRepository.getCustomerTicketByOrderNumber(orderNumber);
        assertThat(customerTickets).size().isEqualTo(2);
        CustomerTicket actual = customerTickets.get(0);
        assertThat(actual.getOrderNumber()).isEqualTo(orderNumber);
    }
 
    @Test
    public void testFindCustomerTicketByNonExistedOrderNumber() throws Exception {              
        this.entityManager.persist(new CustomerTicket(1L, "Order00001", "DemoCustomerTicket1", new Date()));
        this.entityManager.persist(new CustomerTicket(2L, "Order00002", "DemoCustomerTicket2", new Date()));
        
        List<CustomerTicket> customerTickets = this.customerTicketRepository.getCustomerTicketByOrderNumber("Order00003");
        assertThat(customerTickets).size().isEqualTo(0);
    }
}

這里可以看到,我們使用了 @DataJpaTest 實(shí)現(xiàn) CustomerRepository 的注入。同時(shí),我們還注意到另一個(gè)核心測(cè)試組件 TestEntityManager,它的效果相當(dāng)于不使用真正的 CustomerRepository 完成數(shù)據(jù)的持久化,從而提供了一種數(shù)據(jù)與環(huán)境之間的隔離機(jī)制。

執(zhí)行這些測(cè)試用例后,我們需要關(guān)注它們的控制臺(tái)日志輸入,其中核心日志如下所示(為了顯示做了簡(jiǎn)化處理):

Hibernate: drop table customer_ticket if exists
Hibernate: drop sequence if exists hibernate_sequence
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: create table customer_ticket (id bigint not null, account_id bigint, create_time timestamp, description varchar(255), order_number varchar(255), primary key (id))
Hibernate: create table localaccount (id bigint not null, account_code varchar(255), account_name varchar(255), primary key (id))
…
Hibernate: call next value for hibernate_sequence
Hibernate: call next value for hibernate_sequence
Hibernate: insert into customer_ticket (account_id, create_time, description, order_number, id) values (?, ?, ?, ?, ?)
Hibernate: insert into customer_ticket (account_id, create_time, description, order_number, id) values (?, ?, ?, ?, ?)
Hibernate: select customerti0_.id as id1_0_, customerti0_.account_id as account_2_0_, customerti0_.create_time as create_t3_0_, customerti0_.description as descript4_0_, customerti0_.order_number as order_nu5_0_ from customer_ticket customerti0_ where customerti0_.order_number=?
…
Hibernate: drop table customer_ticket if exists
Hibernate: drop sequence if exists hibernate_sequence

從以上日志中,我們不難看出執(zhí)行各種 SQL 語(yǔ)句的效果。

Service層和Controller的測(cè)試

與位于底層的數(shù)據(jù)訪問(wèn)層不同,這兩層的組件都依賴于它的下一層組件,即 Service 層依賴于數(shù)據(jù)訪問(wèn)層,而 Controller 層依賴于 Service 層。因此,對(duì)這兩層進(jìn)行測(cè)試時(shí),我們將使用不同的方案和技術(shù)。

使用 Environment 測(cè)試配置信息

在 Spring Boot 應(yīng)用程序中,Service 層通常依賴于配置文件,所以我們也需要對(duì)配置信息進(jìn)行測(cè)試。

配置信息的測(cè)試方案分為兩種,第一種依賴于物理配置文件,第二種則是在測(cè)試時(shí)動(dòng)態(tài)注入配置信息。

第一種測(cè)試方案比較簡(jiǎn)單,在 src/test/resources 目錄下添加配置文件時(shí),Spring Boot 能讀取這些配置文件中的配置項(xiàng)并應(yīng)用于測(cè)試案例中。

在介紹具體的實(shí)現(xiàn)過(guò)程之前,我們有必要先來(lái)了解一下 Environment 接口,該接口定義如下:

public interface Environment extends PropertyResolver {
    String[] getActiveProfiles();
    String[] getDefaultProfiles();
    boolean acceptsProfiles(String... profiles);
}

在上述代碼中我們可以看到,Environment 接口的主要作用是處理 Profile,而它的父接口 PropertyResolver 定義如下代碼所示:

public interface PropertyResolver {
    boolean containsProperty(String key);
    String getProperty(String key);
    String getProperty(String key, String defaultValue);
    <T> T getProperty(String key, Class<T> targetType);
    <T> T getProperty(String key, Class<T> targetType, T defaultValue);
    String getRequiredProperty(String key) throws IllegalStateException;
    <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;
    String resolvePlaceholders(String text);
    String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
}

顯然,PropertyResolver 的作用是根據(jù)各種配置項(xiàng)的 Key 獲取配置屬性值。

現(xiàn)在,假設(shè) src/test/resources 目錄中的 application.properties 存在如下配置項(xiàng):

springcss.order.point = 10

那么,我們就可以設(shè)計(jì)如下所示的測(cè)試用例了。

@RunWith(SpringRunner.class)
@SpringBootTest
public class EnvironmentTests{
    @Autowired
    public Environment environment;
    @Test
    public void testEnvValue(){
        Assert.assertEquals(10, Integer.parseInt(environment.getProperty("springcss.order.point"))); 
    }
}

這里我們注入了一個(gè) Environment 接口,并調(diào)用了它的 getProperty 方法來(lái)獲取測(cè)試環(huán)境中的配置信息。

除了在配置文件中設(shè)置屬性,我們也可以使用 @SpringBootTest 注解指定用于測(cè)試的屬性值,示例代碼如下:

@RunWith(SpringRunner.class)
@SpringBootTest(properties = {" springcss.order.point = 10"})
public class EnvironmentTests{
    @Autowired
    public Environment environment;
    @Test
    public void testEnvValue(){
        Assert.assertEquals(10, Integer.parseInt(environment.getProperty("springcss.order.point"))); 
    }
}

使用 Mock 測(cè)試 Service 層

Service 層依賴于數(shù)據(jù)訪問(wèn)層。因此,對(duì) Service 層進(jìn)行測(cè)試時(shí),我們還需要引入新的技術(shù)體系,也就是應(yīng)用非常廣泛的 Mock 機(jī)制。

接下來(lái),我們先看一下 Mock 機(jī)制的基本概念。

Mock 機(jī)制

Mock 的意思是模擬,它可以用來(lái)對(duì)系統(tǒng)、組件或類進(jìn)行隔離。

在測(cè)試過(guò)程中,我們通常關(guān)注測(cè)試對(duì)象本身的功能和行為,而對(duì)測(cè)試對(duì)象涉及的一些依賴,僅僅關(guān)注它們與測(cè)試對(duì)象之間的交互(比如是否調(diào)用、何時(shí)調(diào)用、調(diào)用的參數(shù)、調(diào)用的次數(shù)和順序,以及返回的結(jié)果或發(fā)生的異常等),并不關(guān)注這些被依賴對(duì)象如何執(zhí)行這次調(diào)用的具體細(xì)節(jié)。

因此,Mock 機(jī)制就是使用 Mock 對(duì)象替代真實(shí)的依賴對(duì)象,并模擬真實(shí)場(chǎng)景來(lái)開(kāi)展測(cè)試工作。

使用 Mock 對(duì)象完成依賴關(guān)系測(cè)試的示意圖如下所示:

在這里插入圖片描述

可以看出,在形式上,Mock 是在測(cè)試代碼中直接 Mock 類和定義 Mock 方法的行為,通常測(cè)試代碼和 Mock 代碼放一起。因此,測(cè)試代碼的邏輯從測(cè)試用例的代碼上能很容易地體現(xiàn)出來(lái)。

下面我們一起看一下如何使用 Mock 測(cè)試 Service 層。

使用 Mock

@SpringBootTest 注解中的 SpringBootTest.WebEnvironment.MOCK 選項(xiàng),該選項(xiàng)用于加載 WebApplicationContext 并提供一個(gè) Mock 的 Servlet 環(huán)境,內(nèi)置的 Servlet 容器并沒(méi)有真實(shí)啟動(dòng)。接下來(lái),我們針對(duì) Service 層演示一下這種測(cè)試方式。

首先,我們來(lái)看一種簡(jiǎn)單場(chǎng)景,在 customer-service 中存在如下 CustomerTicketService 類:

@Service
public class CustomerTicketService {
    @Autowired
    private CustomerTicketRepository customerTicketRepository;
    public CustomerTicket getCustomerTicketById(Long id) {
        return customerTicketRepository.getOne(id);
    }
    …
}

這里我們可以看到,以上方法只是簡(jiǎn)單地通過(guò) CustomerTicketRepository 完成了數(shù)據(jù)查詢操作。

顯然,對(duì)以上 CustomerTicketService 進(jìn)行集成測(cè)試時(shí),還需要我們提供一個(gè) CustomerTicketRepository 依賴。

下面,我們通過(guò)以下代碼演示一下如何使用 Mock 機(jī)制完成對(duì) CustomerTicketRepository 的隔離

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class CustomerServiceTests {
    @MockBean
    private CustomerTicketRepository customerTicketRepository;
    @Test
    public void testGetCustomerTicketById() throws Exception {
        Long id = 1L;
       Mockito.when(customerTicketRepository.getOne(id)).thenReturn(new CustomerTicket(1L, 1L, "Order00001", "DemoCustomerTicket1", new Date()));
        CustomerTicket actual = customerTicketService.getCustomerTicketById(id);
        assertThat(actual).isNotNull();
        assertThat(actual.getOrderNumber()).isEqualTo("Order00001");
    }
}

首先,我們通過(guò) @MockBean 注解注入了 CustomerTicketRepository;然后,基于第三方 Mock 框架 Mockito 提供的 when/thenReturn 機(jī)制完成了對(duì) CustomerTicketRepository 中 getCustomerTicketById() 方法的 Mock。

當(dāng)然,如果你希望在測(cè)試用例中直接注入真實(shí)的CustomerTicketRepository,這時(shí)就可以使用@SpringBootTest 注解中的 SpringBootTest.WebEnvironment.RANDOM_PORT 選項(xiàng),示例代碼如下:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CustomerServiceTests {
    @Autowired
    private CustomerTicketRepository customerTicketRepository;
    @Test
    public void testGetCustomerTicketById() throws Exception {
        Long id = 1L;
        CustomerTicket actual = customerTicketService.getCustomerTicketById(id);
        assertThat(actual).isNotNull();
        assertThat(actual.getOrderNumber()).isEqualTo("Order00001");
    }
}

運(yùn)行上述代碼后就會(huì)以一個(gè)隨機(jī)的端口啟動(dòng)整個(gè) Spring Boot 工程,并從數(shù)據(jù)庫(kù)中真實(shí)獲取目標(biāo)數(shù)據(jù)進(jìn)行驗(yàn)證。

以上集成測(cè)試的示例中只包含了對(duì) Repository 層的依賴,而有時(shí)候一個(gè) Service 中可能同時(shí)包含 Repository 和其他 Service 類或組件,下面回到如下所示的 CustomerTicketService 類:

@Service
public class CustomerTicketService {
    @Autowired
    private OrderClient orderClient;
    private OrderMapper getRemoteOrderByOrderNumber(String orderNumber) {
        return orderClient.getOrderByOrderNumber(orderNumber);
    }
    …
}

這里我們可以看到,在該代碼中,除了依賴 CustomerTicketRepository 之外,還同時(shí)依賴了 OrderClient。

請(qǐng)注意:以上代碼中的 OrderClient 是在 customer-service 中通過(guò) RestTemplate 訪問(wèn) order-service 的遠(yuǎn)程實(shí)現(xiàn)類,其代碼如下所示:

@Component
public class OrderClient {
    @Autowired
    RestTemplate restTemplate;
    public OrderMapper getOrderByOrderNumber(String orderNumber) {
        ResponseEntity<OrderMapper> restExchange = restTemplate.exchange(
                "http://localhost:8083/orders/{orderNumber}", HttpMethod.GET, null,
                OrderMapper.class, orderNumber);
         OrderMapper result = restExchange.getBody();
        return result;
    }
}

CustomerTicketService 類實(shí)際上并不關(guān)注 OrderClient 中如何實(shí)現(xiàn)遠(yuǎn)程訪問(wèn)的具體過(guò)程。因?yàn)閷?duì)于集成測(cè)試而言,它只關(guān)注方法調(diào)用返回的結(jié)果,所以我們將同樣采用 Mock 機(jī)制完成對(duì) OrderClient 的隔離。

對(duì) CustomerTicketService 這部分功能的測(cè)試用例代碼如下所示,可以看到,我們采用的是同樣的測(cè)試方式。

@Test
public void testGenerateCustomerTicket() throws Exception {
        Long accountId = 100L;
        String orderNumber = "Order00001";
        Mockito.when(this.orderClient.getOrderByOrderNumber("Order00001"))
            .thenReturn(new OrderMapper(1L, orderNumber, "deliveryAddress"));
        Mockito.when(this.localAccountRepository.getOne(accountId))
            .thenReturn(new LocalAccount(100L, "accountCode", "accountName"));
 
        CustomerTicket actual = customerTicketService.generateCustomerTicket(accountId, orderNumber);
        assertThat(actual.getOrderNumber()).isEqualTo(orderNumber);
}

這里提供的測(cè)試用例演示了 Service 層中進(jìn)行集成測(cè)試的各種手段,它們已經(jīng)能夠滿足一般場(chǎng)景的需要。

測(cè)試 Controller 層

對(duì) Controller 層進(jìn)行測(cè)試之前,我們先來(lái)提供一個(gè)典型的 Controller 類,它來(lái)自 customer-service,如下代碼所示:

@RestController
@RequestMapping(value="customers")
public class CustomerController {
    @Autowired
    private CustomerTicketService customerTicketService; 
    @PostMapping(value = "/{accountId}/{orderNumber}")
    public CustomerTicket generateCustomerTicket( @PathVariable("accountId") Long accountId,
            @PathVariable("orderNumber") String orderNumber) {
        CustomerTicket customerTicket = customerTicketService.generateCustomerTicket(accountId, orderNumber);
        return customerTicket;
    }
}

關(guān)于上述 Controller 類的測(cè)試方法,相對(duì)來(lái)說(shuō)比較豐富,比如有 TestRestTemplate、@WebMvcTest 注解和 MockMvc 這三種,下面我們逐一進(jìn)行講解。

使用 TestRestTemplate

Spring Boot 提供的 TestRestTemplate 與 RestTemplate 非常類似,只不過(guò)它專門用在測(cè)試環(huán)境中。

如果我們想在測(cè)試環(huán)境中使用 @SpringBootTest,則可以直接使用 TestRestTemplate 來(lái)測(cè)試遠(yuǎn)程訪問(wèn)過(guò)程,示例代碼如下:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CustomerController2Tests { 
    @Autowired
    private TestRestTemplate testRestTemplate;
 
    @MockBean
    private CustomerTicketService customerTicketService;
 
    @Test
    public void testGenerateCustomerTicket() throws Exception {
        Long accountId = 100L;
        String orderNumber = "Order00001";
        given(this.customerTicketService.generateCustomerTicket(accountId, orderNumber))
                .willReturn(new CustomerTicket(1L, accountId, orderNumber, "DemoCustomerTicket1", new Date()));
 
        CustomerTicket actual = testRestTemplate.postForObject("/customers/" + accountId+ "/" + orderNumber, null, CustomerTicket.class);
        assertThat(actual.getOrderNumber()).isEqualTo(orderNumber);
    }
}

上述測(cè)試代碼中,首先,我們注意到 @SpringBootTest 注解通過(guò)使用 SpringBootTest.WebEnvironment.RANDOM_PORT 指定了隨機(jī)端口的 Web 運(yùn)行環(huán)境。然后,我們基于 TestRestTemplate 發(fā)起了 HTTP 請(qǐng)求并驗(yàn)證了結(jié)果。

特別說(shuō)明:這里使用 TestRestTemplate 發(fā)起請(qǐng)求的方式與 RestTemplate 完全一致

使用 @WebMvcTest 注解

接下來(lái)測(cè)試方法中,我們將引入一個(gè)新的注解 @WebMvcTest,該注解將初始化測(cè)試 Controller 所必需的 Spring MVC 基礎(chǔ)設(shè)施,CustomerController 類的測(cè)試用例如下所示:

@RunWith(SpringRunner.class)
@WebMvcTest(CustomerController.class)
public class CustomerControllerTestsWithMockMvc { 
    @Autowired
    private MockMvc mvc;
 
    @MockBean
    private CustomerTicketService customerTicketService;
 
    @Test
    public void testGenerateCustomerTicket() throws Exception {
        Long accountId = 100L;
        String orderNumber = "Order00001";
        given(this.customerTicketService.generateCustomerTicket(accountId, orderNumber))
                .willReturn(new CustomerTicket(1L, 100L, "Order00001", "DemoCustomerTicket1", new Date()));
 
        this.mvc.perform(post("/customers/" + accountId+ "/" + orderNumber).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());
    }
}

MockMvc 類提供的基礎(chǔ)方法分為以下 6 種,下面一一對(duì)應(yīng)來(lái)看下。

  • Perform:執(zhí)行一個(gè) RequestBuilder 請(qǐng)求,會(huì)自動(dòng)執(zhí)行 SpringMVC 流程并映射到相應(yīng)的 Controller 進(jìn)行處理。
  • get/post/put/delete:聲明發(fā)送一個(gè) HTTP 請(qǐng)求的方式,根據(jù) URI 模板和 URI 變量值得到一個(gè) HTTP 請(qǐng)求,支持 GET、POST、PUT、DELETE 等 HTTP 方法。
  • param:添加請(qǐng)求參數(shù),發(fā)送 JSON 數(shù)據(jù)時(shí)將不能使用這種方式,而應(yīng)該采用 @ResponseBody 注解。
  • andExpect:添加 ResultMatcher 驗(yàn)證規(guī)則,通過(guò)對(duì)返回的數(shù)據(jù)進(jìn)行判斷來(lái)驗(yàn)證 Controller 執(zhí)行結(jié)果是否正確。
  • andDo:添加 ResultHandler 結(jié)果處理器,比如調(diào)試時(shí)打印結(jié)果到控制臺(tái)。
  • andReturn:最后返回相應(yīng)的 MvcResult,然后執(zhí)行自定義驗(yàn)證或做異步處理。

執(zhí)行該測(cè)試用例后,從輸出的控制臺(tái)日志中我們不難發(fā)現(xiàn),整個(gè)流程相當(dāng)于啟動(dòng)了 CustomerController 并執(zhí)行遠(yuǎn)程訪問(wèn),而 CustomerController 中使用的 CustomerTicketService 則做了 Mock。

顯然,測(cè)試 CustomerController 的目的在于驗(yàn)證其返回?cái)?shù)據(jù)的格式和內(nèi)容。在上述代碼中,我們先定義了 CustomerController 將會(huì)返回的 JSON 結(jié)果,然后通過(guò) perform、accept 和 andExpect 方法模擬了 HTTP 請(qǐng)求的整個(gè)過(guò)程,最終驗(yàn)證了結(jié)果的正確性。

請(qǐng)注意 @SpringBootTest 注解不能和 @WebMvcTest 注解同時(shí)使用。

使用 @AutoConfigureMockMvc 注解

在使用 @SpringBootTest 注解的場(chǎng)景下,如果我們想使用 MockMvc 對(duì)象,那么可以引入 @AutoConfigureMockMvc 注解。

通過(guò)將 @SpringBootTest 注解與 @AutoConfigureMockMvc 注解相結(jié)合,@AutoConfigureMockMvc 注解將通過(guò) @SpringBootTest 加載的 Spring 上下文環(huán)境中自動(dòng)配置 MockMvc 這個(gè)類。

使用 @AutoConfigureMockMvc 注解的測(cè)試代碼如下所示:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class CustomerControllerTestsWithAutoConfigureMockMvc {
 
    @Autowired
    private MockMvc mvc;
 
    @MockBean
    private CustomerTicketService customerTicketService;
 
    @Test
    public void testGenerateCustomerTicket() throws Exception {
        Long accountId = 100L;
        String orderNumber = "Order00001";
        given(this.customerTicketService.generateCustomerTicket(accountId, orderNumber))
                .willReturn(new CustomerTicket(1L, 100L, "Order00001", "DemoCustomerTicket1", new Date()));
 
        this.mvc.perform(post("/customers/" + accountId+ "/" + orderNumber).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());
    }
}

在上述代碼中,我們使用了 MockMvc 工具類完成了對(duì) HTTP 請(qǐng)求的模擬,并基于返回狀態(tài)驗(yàn)證了 Controller 層組件的正確性。

小結(jié)

在這里插入圖片描述

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • Mybatis?大數(shù)據(jù)量批量寫(xiě)優(yōu)化的案例詳解

    Mybatis?大數(shù)據(jù)量批量寫(xiě)優(yōu)化的案例詳解

    這篇文章主要介紹了Mybatis?大數(shù)據(jù)量批量寫(xiě)優(yōu)化的示例代碼,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-05-05
  • spring?boot?使用Mybatis-plus查詢方法解析

    spring?boot?使用Mybatis-plus查詢方法解析

    這篇文章主要介紹了spring?boot?使用Mybatis-plus查詢方法解析,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下
    2022-09-09
  • javac final變量未賦值檢測(cè)案例講解

    javac final變量未賦值檢測(cè)案例講解

    這篇文章主要介紹了javac final變量未賦值檢測(cè)案例講解,通過(guò)本文,我們可以知道Eclipse中報(bào)如下錯(cuò)誤:The blank final field b may not have been
    initialized. 是在Flow階段由AssignAnalyzer檢測(cè)出來(lái)的,需要的朋友可以參考下
    2022-12-12
  • 詳解Java中的延時(shí)隊(duì)列 DelayQueue

    詳解Java中的延時(shí)隊(duì)列 DelayQueue

    這篇文章主要介紹了Java中延時(shí)隊(duì)列 DelayQueue的相關(guān)資料,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下
    2020-12-12
  • Java讀取本地json文件及相應(yīng)處理方法

    Java讀取本地json文件及相應(yīng)處理方法

    今天小編就為大家分享一篇Java讀取本地json文件及相應(yīng)處理方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2018-09-09
  • mybatis多個(gè)接口參數(shù)的注解使用方式(@Param)

    mybatis多個(gè)接口參數(shù)的注解使用方式(@Param)

    這篇文章主要介紹了mybatis多個(gè)接口參數(shù)的注解使用方式(@Param),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-10-10
  • java實(shí)現(xiàn)斐波那契數(shù)列的3種方法

    java實(shí)現(xiàn)斐波那契數(shù)列的3種方法

    這篇文章主要介紹了java實(shí)現(xiàn)斐波那契數(shù)列的3種方法,有需要的朋友可以參考一下
    2014-01-01
  • 詳解Java數(shù)據(jù)結(jié)構(gòu)之平衡二叉樹(shù)

    詳解Java數(shù)據(jù)結(jié)構(gòu)之平衡二叉樹(shù)

    平衡二叉樹(shù)(Balanced?Binary?Tree)又被稱為AVL樹(shù)(有別于AVL算法),且具有以下性質(zhì):它是一?棵空樹(shù)或它的左右兩個(gè)子樹(shù)的高度差的絕對(duì)值不超過(guò)1,并且左右兩個(gè)子樹(shù)都是一棵平衡二叉樹(shù)。本文將詳解介紹一下平衡二叉樹(shù)的原理與實(shí)現(xiàn),需要的可以參考一下
    2022-02-02
  • @RequestParam 參數(shù)偶爾丟失的解決

    @RequestParam 參數(shù)偶爾丟失的解決

    這篇文章主要介紹了@RequestParam 參數(shù)偶爾丟失的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-10-10
  • Spring Boot實(shí)現(xiàn)功能的統(tǒng)一詳解

    Spring Boot實(shí)現(xiàn)功能的統(tǒng)一詳解

    這篇文章主要介紹了Spring Boot統(tǒng)一功能的處理,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2022-06-06

最新評(píng)論