1. Overview
1.概述
These days, we expect to call REST APIs in most of our services. Spring provides a few options for building a REST client, and WebClient is recommended.
如今,我们期望在大多数服务中调用REST API。Spring为构建REST客户端提供了一些选择,WebClient被推荐。
In this quick tutorial, we’ll learn how to unit test services that use WebClient to call APIs.
在这个快速教程中,我们将学习如何单元测试使用WebClient调用API的服务。
2. Mocking
2.嘲讽
We have two main options for mocking in our tests:
在我们的测试中,我们有两个主要的嘲弄选项。
- Use Mockito to mimic the behavior of WebClient
- Use WebClient for real, but mock the service it calls by using MockWebServer (okhttp)
3. Using Mockito
3.使用Mockito
Mockito is the most common mocking library for Java. It’s good at providing pre-defined responses to method calls, but things get challenging when mocking fluent APIs. This is because in a fluent API, a lot of objects pass between the calling code and the mock.
Mockito是Java中最常见的嘲讽库。它善于为方法调用提供预定义的响应,但在模拟流畅的API时,事情就变得很有挑战性。这是因为在流畅的API中,大量的对象在调用代码和模拟对象之间传递。
For example, let’s have an EmployeeService class with a getEmployeeById method fetch data via HTTP using WebClient:
例如,我们有一个EmployeeService类,它有一个getEmployeeById方法,通过HTTP使用WebClient来获取数据。
public class EmployeeService {
public EmployeeService(String baseUrl) {
this.webClient = WebClient.create(baseUrl);
}
public Mono<Employee> getEmployeeById(Integer employeeId) {
return webClient
.get()
.uri("http://localhost:8080/employee/{id}", employeeId)
.retrieve()
.bodyToMono(Employee.class);
}
}
We can use Mockito to mock this:
我们可以用Mockito来模拟这个。
@ExtendWith(MockitoExtension.class)
public class EmployeeServiceTest {
@Test
void givenEmployeeId_whenGetEmployeeById_thenReturnEmployee() {
Integer employeeId = 100;
Employee mockEmployee = new Employee(100, "Adam", "Sandler",
32, Role.LEAD_ENGINEER);
when(webClientMock.get())
.thenReturn(requestHeadersUriSpecMock);
when(requestHeadersUriMock.uri("/employee/{id}", employeeId))
.thenReturn(requestHeadersSpecMock);
when(requestHeadersMock.retrieve())
.thenReturn(responseSpecMock);
when(responseMock.bodyToMono(Employee.class))
.thenReturn(Mono.just(mockEmployee));
Mono<Employee> employeeMono = employeeService.getEmployeeById(employeeId);
StepVerifier.create(employeeMono)
.expectNextMatches(employee -> employee.getRole()
.equals(Role.LEAD_ENGINEER))
.verifyComplete();
}
}
As we can see, we need to provide a different mock object for each call in the chain, with four different when/thenReturn calls required. This is verbose and cumbersome. It also requires us to know the implementation details of how exactly our service uses WebClient, making this a brittle way of testing.
我们可以看到,我们需要为链中的每个调用提供一个不同的模拟对象,需要四个不同的when/thenReturn调用。这样做既啰嗦又麻烦。它还要求我们了解我们的服务到底是如何使用WebClient的实现细节,这使得测试的方式变得很脆弱。
So how can we write better tests for WebClient?
那么,我们如何才能为WebClient编写更好的测试呢?
4. Using MockWebServer
4.使用MockWebServer
MockWebServer, built by the Square team, is a small web server that can receive and respond to HTTP requests.
MockWebServer,由Square团队建立,是一个小型的Web服务器,可以接收和响应HTTP请求。
Interacting with MockWebServer from our test cases allows our code to use real HTTP calls to a local endpoint. We get the benefit of testing the intended HTTP interactions, and none of the challenges of mocking a complex fluent client.
从我们的测试案例中与MockWebServer互动,使我们的代码能够使用真正的HTTP调用到本地端点。我们得到了测试预期的HTTP交互的好处,而没有遇到模拟复杂的流畅客户端的挑战。
Using MockWebServer is recommended by the Spring Team for writing integration tests.
使用 MockWebServer是Spring团队推荐的,用于编写集成测试。。
4.1. MockWebServer Dependencies
4.1 LockWebServer的依赖性
To use MockWebServer, we need to add the Maven dependencies for both okhttp and mockwebserver to our pom.xml:
要使用MockWebServer,我们需要将okhttp和mockwebserver的Maven依赖项添加到我们的pom.xml。
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>4.0.1</version>
<scope>test</scope>
</dependency>
4.2. Adding MockWebServer to Our Test
4.2.将MockWebServer添加到我们的测试中
Let’s test our EmployeeService with MockWebServer:
让我们用MockWebServer测试我们的EmployeeService。
public class EmployeeServiceMockWebServerTest {
public static MockWebServer mockBackEnd;
@BeforeAll
static void setUp() throws IOException {
mockBackEnd = new MockWebServer();
mockBackEnd.start();
}
@AfterAll
static void tearDown() throws IOException {
mockBackEnd.shutdown();
}
}
In the above JUnit Test class, the setUp and tearDown method takes care of creating and shutting down the MockWebServer.
在上述JUnit测试类中,setUp和tearDown方法负责创建和关闭MockWebServer。
The next step is to map the port of the actual REST service call to the MockWebServer’s port:
下一步是将实际REST服务调用的端口映射到MockWebServer的端口:。
@BeforeEach
void initialize() {
String baseUrl = String.format("http://localhost:%s",
mockBackEnd.getPort());
employeeService = new EmployeeService(baseUrl);
}
Now it’s time to create a stub so that the MockWebServer can respond to an HttpRequest.
现在是时候创建一个存根,以便MockWebServer能够响应HttpRequest。
4.3. Stubbing a Response
4.3.存根一个响应
Let’s use MockWebServer’s handy enqueue method to queue a test response on the webserver:
让我们使用MockWebServer的方便的enqueue方法,在Webserver上排队等待测试响应。
@Test
void getEmployeeById() throws Exception {
Employee mockEmployee = new Employee(100, "Adam", "Sandler",
32, Role.LEAD_ENGINEER);
mockBackEnd.enqueue(new MockResponse()
.setBody(objectMapper.writeValueAsString(mockEmployee))
.addHeader("Content-Type", "application/json"));
Mono<Employee> employeeMono = employeeService.getEmployeeById(100);
StepVerifier.create(employeeMono)
.expectNextMatches(employee -> employee.getRole()
.equals(Role.LEAD_ENGINEER))
.verifyComplete();
}
When the actual API call is made from the getEmployeeById(Integer employeeId) method in our EmployeeService class, MockWebServer will respond with the queued stub.
当从我们的EmployeeService类中的getEmployeeById(Integer employeeId)方法中进行实际的API调用时,MockWebServer将用排队的存根进行响应。
4.4. Checking a Request
4.4.检查一个请求
We may also want to make sure that the MockWebServer was sent the correct HttpRequest.
我们可能还想确保MockWebServer被发送正确的HttpRequest。
MockWebServer has a handy method named takeRequest that returns an instance of RecordedRequest:
MockWebServer有一个名为takeRequest的便捷方法,该方法返回一个RecordedRequest的实例。
RecordedRequest recordedRequest = mockBackEnd.takeRequest();
assertEquals("GET", recordedRequest.getMethod());
assertEquals("/employee/100", recordedRequest.getPath());
With RecordedRequest, we can verify the HttpRequest that was received to make sure our WebClient sent it correctly.
通过RecordedRequest,我们可以验证收到的HttpRequest,以确保我们的WebClient正确发送。
5. Conclusion
5.总结
In this article, we demonstrated the two main options available to mock WebClient based REST client code.
在这篇文章中,我们展示了两个主要的选项,可用于模拟WebClient的REST客户端代码。
While Mockito worked, and may be a good option for simple examples, the recommended approach is to use MockWebServer.
虽然Mockito可以工作,而且对于简单的例子来说可能是一个不错的选择,但推荐的方法是使用MockWebServer。
As always, the source code for this article is available over on GitHub.
一如既往,本文的源代码可在GitHub上获得。。