一拒逮、概述
友情提示:Reactive Programming ,翻譯為反應式編程咱娶,又稱為響應式編程米间。國內多數(shù)叫響應式編程,本文我們統(tǒng)一使用響應式膘侮。不過屈糊,比較正確的叫法還是反應式。
Spring Framework 5 在 2017 年 9 月份琼了,發(fā)布了 GA 通用版本逻锐。既然是一個新的大版本夫晌,必然帶來了非常多的改進,其中比較重要的一點昧诱,就是將響應式編程帶入了 Spring 生態(tài)晓淀。也就是說,將響應式編程“真正”帶入了 Java 生態(tài)之中盏档。
在此之前凶掰,相信(include me),對響應式編程的概念是非常模糊的蜈亩。甚至說懦窘,截止到目前 2019 年 11 月份,對于國內的 Java 開發(fā)者稚配,也是知之甚少畅涂。
對于我們來說,最早看到的就是 Spring5 提供了一個新的 Web 框架道川,基于響應式編程的 Spring WebFlux 午衰。至此,SpringMVC 在“干掉” Struts 之后愤惰,難道要開始進入 Spring 自己的兩個 Web 框架的雙雄爭霸苇经?
實際上,WebFlux 在出來的兩年時間里宦言,據(jù)了解到的情況扇单,鮮有項目從采用 SpringMVC 遷移到 WebFlux ,又或者新項目直接采用 WebFlux 奠旺。這又是為什么呢蜘澜?
響應式編程,對我們現(xiàn)有的編程方式响疚,是一場顛覆鄙信,對于框架也是。
- 在 Spring 提供的框架中忿晕,實際并沒有全部實現(xiàn)好對響應式編程的支持装诡。例如說,Spring Transaction 事務組件践盼,在 Spring 5.2 M2 版本鸦采,才提供了支持響應式編程的 ReactiveTransactionManager 事務管理器。
- 更不要說咕幻,Java 生態(tài)常用的框架渔伯,例如說 MyBatis、Jedis 等等肄程,都暫未提供響應式編程的支持锣吼。
所以选浑,WebFlux 想要能夠真正普及到我們的項目中,不僅僅需要 Spring 自己體系中的框架提供對響應式編程的很好的支持玄叠,也需要 Java 生態(tài)中的框架也要做到如此古徒。
即使如此,這也并不妨礙我們來對 WebFlux 進行一個小小的入門读恃。畢竟描函,響應式編程這把火,終將熊熊燃起狐粱。Spring Cloud Gateway即使用的的WebFlux實現(xiàn)。
1.1 響應式編程
簡單地說胆数,響應式編程是關于非阻塞應用程序的肌蜻,這些應用程序是異步的、事件驅動的必尼,并且需要少量的線程來垂直伸縮(即在 JVM 中)蒋搜,而不是水平伸縮(即通過集群)。
以后端 API 請求的處理來舉例子判莉。
在現(xiàn)在主流的編程模型中豆挽,請求是被同步阻塞處理完成,返回結果給前端券盅。
在響應式的編程模型中帮哈,請求是被作為一個事件丟到線程池中執(zhí)行,等到執(zhí)行完畢锰镀,異步回調結果給主線程娘侍,最后返回給前端。
通過這樣的方式泳炉,主線程(實際是多個憾筏,這里只是方便描述哈)不斷接收請求,不負責直接同步阻塞處理花鹅,從而避免自身被阻塞氧腰。
1.2 Reactor 框架
簡單來說,Reactor 說是一個響應式編程框架刨肃,又快又不占用內存的那種古拴。
Reactor 有兩個非常重要的基本概念:
-
Flux ,表示的是包含 0 到 N 個元素的異步序列之景。當消息通知產生時斤富,訂閱者(Subscriber)中對應的方法
#onNext(t)
,#onComplete(t)
和#onError(t)
會被調用。 - Mono 表示的是包含 0 或者 1 個元素的異步序列锻狗。該序列中同樣可以包含與 Flux 相同的三種類型的消息通知满力。
- 同時焕参,F(xiàn)lux 和 Mono 之間可以進行轉換。例如:
- 對一個 Flux 序列進行計數(shù)count操作油额,得到的結果是一個
Mono<Long>
對象叠纷。 - 把兩個 Mono 序列合并在一起,得到的是一個 Flux 對象潦嘶。
- 對一個 Flux 序列進行計數(shù)count操作油额,得到的結果是一個
其實涩嚣,可以先暫時簡單把Mono 理解成 Object ,F(xiàn)lux 理解成 List掂僵。
1.3 Spring WebFlux
Spring 官方文檔對 Spring WebFlux 介紹如下:
Spring Framework 5 提供了一個新的 spring-webflux
模塊航厚。該模塊包含了:
- 對響應式支持的 HTTP 和 WebSocket 客戶端。
- 對響應式支持的 Web 服務器锰蓬,包括 Rest API幔睬、HTML 瀏覽器、WebSocket 等交互方式芹扭。
在服務端方面麻顶,WebFlux 提供了 2 種編程模型(翻譯成使用方式,可能更易懂):
方式一舱卡,基于 Annotated Controller 方式實現(xiàn):基于 @Controller 和 SpringMVC 使用的其它注解辅肾。也就是說,我們大體上可以像使用 SpringMVC 的方式轮锥,使用 WebFlux 矫钓。
方式二,基于函數(shù)式編程方式:函數(shù)式舍杜,Java 8 lambda 表達式風格的路由和處理份汗。可能有點晦澀蝴簇,晚點我們看了示例就會明白杯活。
下面,開始讓我們開始愉快的快速入門熬词。
2. 快速入門
我們會使用 spring-boot-starter-webflux
實現(xiàn) WebFlux 的自動化配置旁钧。然后實現(xiàn)用戶的增刪改查接口。接口列表如下:
請求方法 | URL | 功能 |
---|---|---|
GET |
/users/list |
查詢用戶列表 |
GET |
/users/get |
獲得指定用戶編號的用戶 |
POST |
/users/add |
添加用戶 |
POST |
/users/update |
更新指定用戶編號的用戶 |
POST |
/users/delete |
刪除指定用戶編號的用戶 |
天文1號不是發(fā)射了嗎互拾!下面歪今,開始神秘的火星之旅~
2.1 引入依賴
在IDEA中,要創(chuàng)建WebFlux
項目颜矿,必須勾選Spring Reactive Web
而不是傳統(tǒng)的Spring Web
寄猩,這里為了簡化代碼使用到了Lombok。
在 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>webflux</artifactId>
<dependencies>
<!-- 實現(xiàn)對 Spring WebFlux 的自動化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>2.3.0.RELEASE</version>
</dependency>
<!-- 方便等會寫單元測試 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
2.2 Application
創(chuàng)建 Application.java
類替废,配置 @SpringBootApplication
注解即可。
package com.erbadagang.springboot.springwebflux;
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);
}
}
2.3 基于 Annotated Controller 方式實現(xiàn)
創(chuàng)建 [UserController] 類泊柬。代碼如下:
package com.erbadagang.springboot.springwebflux.controller;
import com.erbadagang.springboot.springwebflux.dto.UserAddDTO;
import com.erbadagang.springboot.springwebflux.dto.UserUpdateDTO;
import com.erbadagang.springboot.springwebflux.service.UserService;
import com.erbadagang.springboot.springwebflux.vo.UserVO;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
/**
* 用戶 Controller
*/
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
/**
* 查詢用戶列表
*
* @return 用戶列表
*/
@GetMapping("/list")
public Flux<UserVO> list() {
// 查詢列表
List<UserVO> result = new ArrayList<>();
result.add(new UserVO().setId(1).setUsername("yudaoyuanma"));
result.add(new UserVO().setId(2).setUsername("woshiyutou"));
result.add(new UserVO().setId(3).setUsername("chifanshuijiao"));
// 返回列表
return Flux.fromIterable(result);
}
/**
* 獲得指定用戶編號的用戶
*
* @param id 用戶編號
* @return 用戶
*/
@GetMapping("/get")
public Mono<UserVO> get(@RequestParam("id") Integer id) {
// 查詢用戶
UserVO user = new UserVO().setId(id).setUsername("username:" + id);
// 返回
return Mono.just(user);
}
/**
* 獲得指定用戶編號的用戶
*
* @param id 用戶編號
* @return 用戶
*/
@GetMapping("/v2/get")
public Mono<UserVO> get2(@RequestParam("id") Integer id) {
// 查詢用戶
UserVO user = userService.get(id);
// 返回
return Mono.just(user);
}
/**
* 添加用戶
*
* @param addDTO 添加用戶信息 DTO
* @return 添加成功的用戶編號
*/
@PostMapping("add")
public Mono<Integer> add(@RequestBody Publisher<UserAddDTO> addDTO) {
// 插入用戶記錄椎镣,返回編號
Integer returnId = 1;
// 返回用戶編號
return Mono.just(returnId);
}
/**
* 添加用戶
*
* @param addDTO 添加用戶信息 DTO
* @return 添加成功的用戶編號
*/
@PostMapping("add2")
public Mono<Integer> add2(Mono<UserAddDTO> addDTO) {
// 插入用戶記錄,返回編號
Integer returnId = 1;
// 返回用戶編號
return Mono.just(returnId);
}
/**
* 更新指定用戶編號的用戶
*
* @param updateDTO 更新用戶信息 DTO
* @return 是否修改成功
*/
@PostMapping("/update")
public Mono<Boolean> update(@RequestBody Publisher<UserUpdateDTO> updateDTO) {
// 更新用戶記錄
Boolean success = true;
// 返回更新是否成功
return Mono.just(success);
}
/**
* 刪除指定用戶編號的用戶
*
* @param id 用戶編號
* @return 是否刪除成功
*/
@PostMapping("/delete") // URL 修改成 /delete 兽赁,RequestMethod 改成 DELETE
public Mono<Boolean> delete(@RequestParam("id") Integer id) {
// 刪除用戶記錄
Boolean success = true;
// 返回是否更新成功
return Mono.just(success);
}
}
- 在類和方法上状答,我們添加了
@Controller
和 SpringMVC 在使用的@GetMapping
和@PostMapping
等注解,提供 API 接口刀崖,這個和我們在使用 SpringMVC 是一模一樣的惊科。 - 在
dto
和vo
包下,有 API 使用到的 DTO 和 VO 類亮钦。 - 因為是入門示例译断,我們會發(fā)現(xiàn)代碼十分簡單,淡定或悲,淡定(讓我想起來Trump跟記者打架讓閉嘴,shutup堪唐,shutup......)巡语。在后文中,我們會提供和 Spring Data JPA淮菠、Spring Data Redis 等等整合的示例男公。
-
#list()
方法,我們最終調用Flux#fromIterable(Iterable<? extends T> it)
方法合陵,將 List 包裝成 Flux 對象返回枢赔。 -
#get(Integer id)
方法,我們最終調用Mono#just(T data)
方法拥知,將 UserVO 包裝成 Mono 對象返回踏拜。 -
#add(Publisher<UserAddDTO> addDTO)
方法,參數(shù)為 Publisher 類型低剔,泛型為 UserAddDTO 類型速梗,并且添加了@RequestBody
注解,從 request 的 Body 中讀取參數(shù)襟齿。注意姻锁,此時提交參數(shù)需要使用"application/json"
等 Content-Type 內容類型。 -
#add(...)
方法猜欺,也可以使用application/x-www-form-urlencoded
或multipart/form-data
這兩個 Content-Type 內容類型位隶,通過 request 的 Form Data 或 Multipart Data 傳遞參數(shù)。代碼如下:
// UserController.java
/**
* 添加用戶
*
* @param addDTO 添加用戶信息 DTO
* @return 添加成功的用戶編號
*/
@PostMapping("add2")
public Mono<Integer> add(Mono<UserAddDTO> addDTO) {
// 插入用戶記錄开皿,返回編號
Integer returnId = UUID.randomUUID().hashCode();
// 返回用戶編號
return Mono.just(returnId);
}
此時涧黄,參數(shù)為 Mono 類型篮昧,泛型為 UserAddDTO 類型。
當然弓熏,我們也可以直接使用參數(shù)為 UserAddDTO 類型挟冠。如果后續(xù)需要使用到 Reactor API 褒墨,則我們自己主動調用 Mono#just(T data) 方法,封裝出 Publisher 對象。注意羡榴,F(xiàn)lux 和 Mono 都實現(xiàn)了 Publisher 接口。
-
#update(Publisher<UserUpdateDTO> updateDTO)
方法惧磺,和#add(Publisher<UserAddDTO> addDTO)
方法一致青责,就不重復贅述。 -
#delete(Integer id)
方法嫁赏,和#get(Integer id)
方法一致其掂,就不重復贅述。
2.4 基于函數(shù)式編程方式
創(chuàng)建 [UserRouter]類潦蝇。代碼如下:
package com.erbadagang.springboot.springwebflux.controller;
import com.erbadagang.springboot.springwebflux.vo.UserVO;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.*;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.*;
import static org.springframework.web.reactive.function.server.ServerResponse.*;
/**
* 用戶 Router
*/
@Configuration
public class UserRouter {
@Bean
public RouterFunction<ServerResponse> userListRouterFunction() {
return RouterFunctions.route(RequestPredicates.GET("/users2/list"),
new HandlerFunction<ServerResponse>() {
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
// 查詢列表
List<UserVO> result = new ArrayList<>();
result.add(new UserVO().setId(1).setUsername("yudaoyuanma"));
result.add(new UserVO().setId(2).setUsername("woshiyutou"));
result.add(new UserVO().setId(3).setUsername("chifanshuijiao"));
// 返回列表
return ServerResponse.ok().bodyValue(result);
}
});
}
@Bean
public RouterFunction<ServerResponse> userGetRouterFunction() {
return RouterFunctions.route(RequestPredicates.GET("/users2/get"),
new HandlerFunction<ServerResponse>() {
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
// 獲得編號
Integer id = request.queryParam("id")
.map(s -> StringUtils.isEmpty(s) ? null : Integer.valueOf(s)).get();
// 查詢用戶
UserVO user = new UserVO().setId(id).setUsername(UUID.randomUUID().toString());
// 返回列表
return ServerResponse.ok().bodyValue(user);
}
});
}
@Bean
public RouterFunction<ServerResponse> demoRouterFunction() {
return route(GET("/users2/demo"), request -> ok().bodyValue("demo"));
}
}
在類上款熬,添加
@Configuration
注解,保證該類中的 Bean 們攘乒,都被掃描到贤牛。-
在每個方法中,我們都通弄
RouterFunctions#route(RequestPredicate predicate, HandlerFunction<T> handlerFunction)
方法则酝,定義了一條路由殉簸。- 第一個參數(shù)
predicate
參數(shù),是 RequestPredicate 類型沽讹,請求謂語般卑,用于匹配請求∷郏可以通過 RequestPredicates 來構建各種條件蝠检。 - 第二個參數(shù)
handlerFunction
參數(shù),是 RouterFunction 類型挚瘟,處理器函數(shù)蝇率。
- 第一個參數(shù)
每個方法定義的路由,胖友自己看下代碼刽沾,一眼能看的明白本慕。一般來說,采用第三個方法的寫法侧漓,更加簡潔锅尘。注意,需要使用
static import
靜態(tài)引入,代碼如下:
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.*;
import static org.springframework.web.reactive.function.server.ServerResponse.*;
加推薦基于 Annotated Controller 方式實現(xiàn)的編程方式藤违,更符合我們現(xiàn)在的開發(fā)習慣浪腐,學習成本也相對低一些。同時顿乒,和 API 接口文檔工具 Swagger 也更容易集成议街。
3. 測試接口
在開發(fā)完接口,我們會進行接口的自測璧榄。一般情況下特漩,我們先啟動項目,然后使用 Postman骨杂、curl涂身、瀏覽器,手工模擬請求后端 API 接口搓蚪。
如訪問url
實際上蛤售,WebFlux 提供了 Web 測試客戶端 WebTestClient 類,方便我們快速測試接口妒潭。下面悴能,我們對 UserController提供的接口,進行下單元測試雳灾。
MockMvc 提供了集成測試和單元測試的能力漠酿。
3.1 集成測試
創(chuàng)建 [UserControllerTest]測試類,我們來測試一下簡單的 UserController 的每個操作佑女。核心代碼如下:
// UserControllerTest.java
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@AutoConfigureWebFlux
@AutoConfigureWebTestClient
public class UserControllerTest {
@Autowired
private WebTestClient webClient;
@Test
public void testList() {
webClient.get().uri("/users/list")
.exchange() // 執(zhí)行請求
.expectStatus().isOk() // 響應狀態(tài)碼 200
.expectBody().json("[\n" +
" {\n" +
" \"id\": 1,\n" +
" \"username\": \"yudaoyuanma\"\n" +
" },\n" +
" {\n" +
" \"id\": 2,\n" +
" \"username\": \"woshiyutou\"\n" +
" },\n" +
" {\n" +
" \"id\": 3,\n" +
" \"username\": \"chifanshuijiao\"\n" +
" }\n" +
"]"); // 響應結果
}
@Test
public void testGet() {
// 獲得指定用戶編號的用戶
webClient.get().uri("/users/get?id=1")
.exchange() // 執(zhí)行請求
.expectStatus().isOk() // 響應狀態(tài)碼 200
.expectBody().json("{\n" +
" \"id\": 1,\n" +
" \"username\": \"username:1\"\n" +
"}"); // 響應結果
}
@Test
public void testGet2() {
// 獲得指定用戶編號的用戶
webClient.get().uri("/users/v2/get?id=1")
.exchange() // 執(zhí)行請求
.expectStatus().isOk() // 響應狀態(tài)碼 200
.expectBody().json("{\n" +
" \"id\": 1,\n" +
" \"username\": \"test\"\n" +
"}"); // 響應結果
}
@Test
public void testAdd() {
Map<String, Object> params = new HashMap<>();
params.put("username", "yudaoyuanma");
params.put("password", "nicai");
// 添加用戶
webClient.post().uri("/users/add")
.bodyValue(params)
.exchange() // 執(zhí)行請求
.expectStatus().isOk() // 響應狀態(tài)碼 200
.expectBody().json("1"); // 響應結果。因為沒有提供 content 的比較谈竿,所以只好使用 json 來比較团驱。竟然能通過
}
@Test
public void testAdd2() { // 發(fā)送文件的測試,可以參考 https://dev.to/shavz/sending-multipart-form-data-using-spring-webtestclient-2gb7 文章
BodyInserters.FormInserter<String> formData = // Form Data 數(shù)據(jù)空凸,需要這么拼湊
BodyInserters.fromFormData("username", "yudaoyuanma")
.with("password", "nicai");
// 添加用戶
webClient.post().uri("/users/add2")
.body(formData)
.exchange() // 執(zhí)行請求
.expectStatus().isOk() // 響應狀態(tài)碼 200
.expectBody().json("1"); // 響應結果嚎花。因為沒有提供 content 的比較,所以只好使用 json 來比較呀洲。竟然能通過
}
@Test
public void testUpdate() {
Map<String, Object> params = new HashMap<>();
params.put("id", 1);
params.put("username", "yudaoyuanma");
// 修改用戶
webClient.post().uri("/users/update")
.bodyValue(params)
.exchange() // 執(zhí)行請求
.expectStatus().isOk() // 響應狀態(tài)碼 200
.expectBody(Boolean.class) // 期望返回值類型是 Boolean
.consumeWith((Consumer<EntityExchangeResult<Boolean>>) result -> // 通過消費結果紊选,判斷符合是 true 。
Assert.assertTrue("返回結果需要為 true", result.getResponseBody()));
}
@Test
public void testDelete() {
// 刪除用戶
webClient.post().uri("/users/delete?id=1")
.exchange() // 執(zhí)行請求
.expectStatus().isOk() // 響應狀態(tài)碼 200
.expectBody(Boolean.class) // 期望返回值類型是 Boolean
.isEqualTo(true); // 這樣更加簡潔一些
// .consumeWith((Consumer<EntityExchangeResult<Boolean>>) result -> // 通過消費結果道逗,判斷符合是 true 兵罢。
// Assert.assertTrue("返回結果需要為 true", result.getResponseBody()));
}
}
- 在類上,我們添加了
@AutoConfigureWebTestClient
注解滓窍,用于自動化配置我們稍后注入的 WebTestClient Bean 對象webClient
卖词。在后續(xù)的測試中,我們會看到都是通過webClient
調用后端 API 接口吏夯。而每一次調用后端 API 接口此蜈,都會執(zhí)行真正的后端邏輯即横。因此,整個邏輯裆赵,走的是集成測試东囚,會啟動一個真實的 Spring 環(huán)境。 - 每次 API 接口的請求战授,都通過 RequestHeadersSpec 來構建页藻。構建完成后,通過
RequestHeadersSpec#exchange()
方法來執(zhí)行請求陈醒,返回 ResponseSpec 結果惕橙。- WebTestClient 的
#get()
、#head()
钉跷、#delete()
弥鹦、#options()
方法,返回的是 RequestHeadersUriSpec 對象爷辙。 - WebTestClient 的
#post()
彬坏、#put()
、#delete()
膝晾、#patch()
方法栓始,返回的是 RequestBodyUriSpec 對象。 - RequestHeadersUriSpec 和 RequestBodyUriSpec 都繼承了 RequestHeadersSpec 接口血当。
- WebTestClient 的
- 執(zhí)行完請求后幻赚,通過調用 RequestBodyUriSpec 的各種斷言方法,添加對結果的預期臊旭,相當于做斷言落恼。如果不符合預期,則會拋出異常离熏,測試不通過佳谦。
3.2 單元測試
為了更好的展示 WebFlux 單元測試的示例,我們需要改寫 UserController 的代碼滋戳,讓其會依賴 UserService 钻蔑。修改點如下:
- 創(chuàng)建 [UserService]類。代碼如下:
// UserService.java
@Service
public class UserService {
public UserVO get(Integer id) {
return new UserVO().setId(id).setUsername("test");
}
}
- 在 [UserController]類中奸鸯,增加
GET /users/v2/get
接口咪笑,獲得指定用戶編號的用戶。代碼如下:
// UserController.java
@Autowired
private UserService userService;
/**
* 獲得指定用戶編號的用戶
*
* @param id 用戶編號
* @return 用戶
*/
@GetMapping("/v2/get")
public Mono<UserVO> get2(@RequestParam("id") Integer id) {
// 查詢用戶
UserVO user = userService.get(id);
// 返回
return Mono.just(user);
}
在代碼中娄涩,我們注入了 UserService Bean 對象 userService 蒲肋,然后在新增的接口方法中,會調用 UserService#get(Integer id) 方法,獲得指定用戶編號的用戶兜粘。
創(chuàng)建 [UserControllerTest2]測試類申窘,我們來測試一下簡單的 UserController 的新增的這個 API 操作。代碼如下:
// UserControllerTest2.java
@RunWith(SpringRunner.class)
@WebFluxTest(UserController.class)
public class UserControllerTest2 {
@Autowired
private WebTestClient webClient;
@MockBean
private UserService userService;
@Test
public void testGet2() throws Exception {
// Mock UserService 的 get 方法
System.out.println("before mock:" + userService.get(1)); // <1.1>
Mockito.when(userService.get(1)).thenReturn(
new UserVO().setId(1).setUsername("username:1")); // <1.2>
System.out.println("after mock:" + userService.get(1)); // <1.3>
// 查詢用戶列表
webClient.get().uri("/users/v2/get?id=1")
.exchange() // 執(zhí)行請求
.expectStatus().isOk() // 響應狀態(tài)碼 200
.expectBody().json("{\n" +
" \"id\": 1,\n" +
" \"username\": \"username:1\"\n" +
"}"); // 響應結果
}
}
- 在類上添加
@WebFluxTest
注解孔轴,并且傳入的是 UserController 類剃法,表示我們要對 UserController 進行單元測試。 - 同時路鹰,
@WebFluxTest
注解贷洲,是包含了@UserController
的組合注解,所以它會自動化配置我們稍后注入的 WebTestClient Bean 對象mvc
晋柱。在后續(xù)的測試中优构,我們會看到都是通過webClient
調用后端 API 接口。但是雁竞!每一次調用后端 API 接口钦椭,并不會執(zhí)行真正的后端邏輯,而是走的 Mock 邏輯碑诉。也就是說彪腔,整個邏輯,走的是單元測試进栽,只會啟動一個 Mock 的 Spring 環(huán)境德挣。
注意上面每個加粗的地方!
-
userService
屬性快毛,我們添加了 [@MockBean
]注解格嗅,實際這里注入的是一個使用 Mockito 創(chuàng)建的 UserService Mock 代理對象。如下圖所示:[圖片上傳失敗...(image-46836b-1596810612894)]UserController 中唠帝,也會注入一個 UserService 屬性屯掖,此時注入的就是該 Mock 出來的 UserService Bean 對象。
默認情況下没隘,
<1.1>
處懂扼,我們調用UserService#get(Integer id)
方法禁荸,然后打印返回結果右蒲。執(zhí)行結果如下:before mock:null
, 結果竟然返回的是null
空。理論來說赶熟,此時應該返回一個id = 1
的 UserVO 對象瑰妄。實際上,因為此時的userService
是通過 Mockito 來 Mock 出來的對象映砖,其所有調用它的方法间坐,返回的都是空。<1.2>
處,通過 Mockito 進行 MockuserService
的#get(Integer id)
方法竹宋,當傳入的id = 1
方法參數(shù)時劳澄,返回id = 1
并且username = "username:1"
的 UserVO 對象。<1.3>
處蜈七,再次調用UserService#get(Integer id)
方法秒拔,然后打印返回結果。執(zhí)行結果如下:after cn.iocoder.springboot.lab27.springwebflux.vo.UserVO@23202c31
打印的就是我們 Mock 返回的 UserVO 對象飒硅。
后續(xù)砂缩,使用
webClient
完成一次后端 API 調用,并進行斷言結果是否正確三娩。執(zhí)行成功庵芭,單元測試通過。
底線
本文源代碼使用 Apache License 2.0開源許可協(xié)議雀监,這里是本文源碼Gitee地址双吆,可通過命令git clone+地址
下載代碼到本地,也可直接點擊鏈接通過瀏覽器方式查看源代碼滔悉。