原文由 Trisha Gee 在当地时间2019年11月4日发布在 INTELLIJ IDEA BLOG
这是第二步,演示如何创建一个 Java 客户端连接到一个发送一系列服务端发送事件的流。我们将使用测试驱动开发来进行开发客户端并进行测试。视频在 B 站
本教程是一系列视频,概述了构建完整的Spring Boot的许多步骤,具有 Kotlin 服务后端,Java 客户端和 JavaFX 用户界面的应用程序。
第二个视频将展示如何创建。一个响应式Spring Java客户端,连接到每秒流式传输股票价格的REST服务。
创建客户端工程
我们将为这客户端创建一个新工程,我们希望将客户端和服务器代码完全分开,因为它们应该完全独立运行的。
- 这个工程包含多个模块,所以开始的时候选择创建空工程。
- 将工程命名为 stock-client 按 Finish
- 默认情况下,当创建一个新的空 Project 时,IntelliJ IDEA显示 Project Structure 对话框的,Modules 部分。我们将在此处添加一个新模块,这将是一个 Spring Boot模块,因此我们选择左边的 Spring Initializr。
- SDK我们使用 Java 13,但没有使用新的特性。
- 输入组和工件的详细信息,我们称此模块为stock-client。
- 我们将为模块填入一个有用的描述,以便清楚了解此代码的用途。
- 我们将保留默认使用Java创建Maven项目
- 选择Java 11作为版本,因为这是当前的长期支持版本。
- 我们可以选择更改默认的包结构。
接下来选择所选的 Spring Boot Starter
- 使用Spring Boot 2.2.0 RC1
- 选择Spring Reactive Web Starter,然后也选择Lombok。
- 默认模块名称和位置没问题, 保留不变。
IntelliJ IDEA 从 Spring Initializr 获取工程,并适当地设置IDE。选择 enable auto-import
创建客户端类
- 删除Spring Initializr为我们创建的
StockClientApplication
,在这个模块我们不需要它,因为该模块将成为其他模块的库。 - 创建一个类WebClientStockClient。它将使用Spring的WebClient来,连接到股票价格服务。
1
2
3
public class WebClientStockClient {
}
创建客户端的测试
驱动客户端需求并验证可行性的方法之一是,是以测试驱动的方式进行开发。
- Windows 或 Linux 使用 Ctrl+Shift+T (macOS 使用⇧⌘T ) 我们可以导航到某个类的测试。 在
WebClientStockClient
这个类还没有测试 所以让我们创建一个。 - 选择 JUnit 5 作为测试框架。
- 这实际上会是一个端到端测试,所以填入
WebClientStockClientIntegrationTest
作为类名。 - 用快捷键 Alt+Insert (⌘N) 生成测试的方法,在生成菜单中选择 “Test Method” 。
- 这不会是测试驱动开发的完美示例,因为我们只是创建一个只测试最佳情况的测试,有时称为快乐路径测试。将测试命名为像
shouldRetrieveStockPricesFromTheService
- 为测试创建一个
WebClientStockClient
实例
1
2
3
4
5
6
class WebClientStockClientIntegrationTest {
@Test
void shouldRetrieveStockPricesFromTheService() {
WebClientStockClient webClientStockClient = new WebClientStockClient();
}
}
我们可以通过测试驱动来做的其中之一,是按照我们想要的API进行编码,而不是测试我们已经创建的东西。IntelliJ IDEA 使得这样的操作更加容易,因为我们可以创建我们想要的测试,并为其生成代码,通常是使用 Alt + Enter
- 在测试代码中,在 WebClientStockClient 上调用 pricesFor 方法。 这个方法需要一个 String 类型的参数表示想要了解其价格股票的代码。
1
2
3
4
void shouldRetrieveStockPricesFromTheService() {
WebClientStockClient webClientStockClient = new WebClientStockClient();
webClientStockClient.pricesFor("SYMBOL");
}
(注意:此代码当前未能通过编译)
在客户端中创建一个基本的价格方法
- (提示:在红色的 pricesFor 方法上按下 Alt + Enter 去让 IntelliJ IDEA 在
WebClientStockClient
里边创建这个方法,并有符合预期的签名。) - 将 WebClientStockClient 里的方法返回值类改成
Flux<StockPrice>
Flux - 最简单地创建可编译、测试的方法是,让此方法返回空的Flux:
1
2
3
4
5
public class WebClientStockClient {
public Flux<StockPrice> pricesFor(String symbol) {
return Flux.fromArray(new StockPrice[0]);
}
}
(注意:此代码当前未能通过编译)
创建一个类保存股票价格
- (提示:最简单的方法是使用Alt + Enter让IntelliJ IDEA创建类)
- 在与 WebClientStockClient 的包里创建 StockPrice
这就是我们要使用Lombok的地方,使用Lombok的@Data注解,我们可以创建类似于第一个视频中的Kotlin数据类。通过使用@Data注解,我们只需要使用字段定义该类的属性,getters,setters,equals,hashCode,以及toString方法均由Lombok提供。
使用Lombok IntelliJ IDEA插件获得代码补全和其他有用的功能。
添加 String symbol,Double price 以及 LocalDateTime time 到 StockPrice 类。
通过Lombok添加@AllArgsConstructor和@NoArgsConstructor,这对于我们的代码是必需的
并用于JSON序列化。
1
2
3
4
5
6
7
8
@Data
@AllArgsConstructor
@NoArgsConstructor
public class StockPrice {
private String symbol;
private Double price;
private LocalDateTime time;
}
为测试添加断言
我们回到 WebClientStockClientIntegrationTest 并添加一些断言, 我们需要检查Flux<StockPrice>
是否符合预期。
- 将返回的 Flux 保存到 prices 局部变量。
- 添加此为非空的断言。
- 添加一个断言,如果如果我们从Flux中取出五个价格,我们不止得到一个价格。
1
2
3
4
5
6
7
8
9
10
11
12
@Test
void shouldRetrieveStockPricesFromTheService() {
// given
WebClientStockClient webClientStockClient = new WebClientStockClient(webClient);
// when
Flux<StockPrice> prices = webClientStockClient.pricesFor("SYMBOL");
// then
Assertions.assertNotNull(prices);
Assertions.assertTrue(prices.take(5).count().block() > 0);
}
当我们运行此测试时,我们看到它失败了,它失败是因为Flux中包含零个元素,因为这是我们硬编码的内容。
将客户端连接到真实的服务
让我们回到 WebClientStockClient 并写入实现。
- 我们要使用WebClient去连接服务。我们将其创建为一个字段,并添加为构造函数参数,以便Spring自动注入。
1
2
3
4
5
6
7
public class WebClientStockClient {
private WebClient webClient;
public WebClientStockClient(WebClient webClient) {
this.webClient = webClient;
}
// ...rest of the class here
现在我们想要使用 WebClient 在我们的方法中调用 REST 服务。
- 移除来自priceFor方法中的桩代码(即删除
return Flux.fromArray(new StockPrice[0]);
) - 我们使用WebClient发出 GET 请求 (get()).
- 传入服务的 URI (http://localhost:8080/stocks/{symbol}) 并传入股票代码(symbol)
- 调用 retrieve().
- 我们要指定如何将这个调用的响应转换为某种类型的 Flux,使用bodyToFlux并将数据类 StockPrice.class 作为参数。
1
2
3
4
5
6
public Flux<StockPrice> pricesFor(String symbol) {
return webClient.get()
.uri("http://localhost:8080/stocks/{symbol}", symbol)
.retrieve()
.bodyToFlux(StockPrice.class);
}
这些是要获得来自GET调用的响应流的最基本的要求,但我们还可以定义诸如retry和backoff策略,请记住了解从发布者到消费者的数据流,是创建成功的响应式应用程序的重要部分。
我们还可以定义抛出特定异常时的处理方式。例如,我们可以说 当我们看到IOException时 我们想记录它。 我们将使用Lombok的Log4j2注解,使我们能够访问日志并记录错误。
这不是处理错误的最可靠的方式,这只是表明我们可以认为异常在响应流中是数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import lombok.extern.log4j.Log4j2;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import java.io.IOException;
import java.time.Duration;
@Log4j2
public class WebClientStockClient {
private WebClient webClient;
public WebClientStockClient(WebClient webClient) {
this.webClient = webClient;
}
public Flux<StockPrice> pricesFor(String symbol) {
return webClient.get()
.uri("http://localhost:8080/stocks/{symbol}", symbol)
.retrieve()
.bodyToFlux(StockPrice.class)
.retryBackoff(5, Duration.ofSeconds(1), Duration.ofSeconds(20))
.doOnError(IOException.class, e -> log.error(e.getMessage()));
}
}
运行集成测试
回到 WebClientStockClientIntegrationTest
,可以看到有些需要修复的东西。
- 我们现在需要给客户端一个
WebClient
, 在测试中将其创建为字段。 - (使用智能补全
Ctrl+Shift+空格
, IntelliJ IDEA 甚至可以建议创建WebClient
实例的完整语句)
1
2
3
4
5
6
7
8
class WebClientStockClientIntegrationTest {
private WebClient webClient = WebClient.builder().build();
@Test
void shouldRetrieveStockPricesFromTheService() {
WebClientStockClient webClientStockClient = new WebClientStockClient(webClient);
// ...其余代码
- 为了进行集成测试,REST 服务必须要运行。回到上一个期创建的
StockServiceApplication
并运行起来。 - 运行
WebClientStockClientIntegrationTest
。 你可以用边栏上的图标或使用快捷键Ctrl+Shift+F10
(macOS 快捷键是 ⌃⇧R ) ,或者双击Ctrl
(“run anything”) 然后输入测试的名称。
现在我们应该可以看到测试为绿色通过。如果我们看一下输出,可以看到我们正在解码带有符号的 StockPrice
对象,随机价格和时间。
更多关于在集成测试中使用断言
这不是最彻底的测试,所以让我们为的断言添加更多细节,以确保客户端符合我们预期。让我们更改断言为要获取五个价格时要求有五个价格,并确保某股票价格的代号是我们所期望的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
class WebClientStockClientIntegrationTest {
private WebClient webClient = WebClient.builder().build();
@Test
void shouldRetrieveStockPricesFromTheService() {
// given
WebClientStockClient webClientStockClient = new WebClientStockClient(webClient);
// when
Flux<StockPrice> prices = webClientStockClient.pricesFor("SYMBOL");
// then
Assertions.assertNotNull(prices);
Flux<StockPrice> fivePrices = prices.take(5);
Assertions.assertEquals(5, fivePrices.count().block());
Assertions.assertEquals("SYMBOL", fivePrices.blockFirst().getSymbol());
}
}
总结
测试响应式应用程序是一项技能,而且还有比我们所展示的更好的方法。但是,我们已经成功地使用了集成测试来驱动股票价格客户端的API和功能,该客户端连接到发出服务器发送事件,并返回Flux<StockPrice>
对象可被其他服务消费的端点。在本教程的后续视频中,我们将展示如何执行此操作。