Dev Notebook: Mockito witchcraft for JAI Java AI (Open AI API Client)
Mockito to mock HttpClient and Completable Future
I was considering different frameworks for testing JAI, the Java Open AI API Client, but I want a build that runs fast so that I can iterate on JAI quickly. While I was thinking about JUnit unit test, I wanted to test via a mock of the HttpClient for speed. Before that, I considered several options, including:
Ultimately, I decided to use Mockito and CompletableFuture to create a mock HTTP client. Recently, I was on several projects that used WireMock and Testcontainers, and while useful for integration testing, the combination makes the unit testing crawl—too slow for my attention span.
You might see some better code listings and tables here.
?? Wiremock, Testcontainers, and Awaitility are all possible topics for future developer notebook articles. I am interested in them and have used them quite a bit for other projects
To set up my development environment, I downloaded and set up Java 11 and added Mockito to the project via build.gradle (but since I often have to use Maven, I included instructions for pom.xml as well). I used Mockito to create mock objects and define their behavior, then used CompletableFuture with the HttpClient to make asynchronous HTTP requests. I also covered best practices for using Mockito, and practical applications of this knowledge.
This developer notebook entry provides a high-level overview of how to use Mockito and CompletableFuture in Java 11 to create a mock HTTP client. I also cover setting up your development environment, creating basic Mockito mock objects, and using CompletableFuture with the HttpClient to make asynchronous HTTP requests. The tutorial also covers best practices for using Mockito, common pitfalls to avoid, and practical applications of this knowledge. By the end of the tutorial, you will have a solid foundation in using Mockito to create mock objects and test your Java code effectively.
?? If you are an old Mockito pro, or perhaps you love EasyMock, I’d love to hear your ideas. Feel free to tell me that I am doing it wrong. ???Constructive criticism is appreciated, and un-constructive criticism is tolerated.
Let’s try to cover the following concepts.
1. Overview
2. Setting up the environment
3. Basics of Mockito
4. Basics of CompletableFuture
5. Basics of HttpClient in Java 11
7. Creating a Mock HttpClient
8. Mocking HttpClient methods with Mockito
9. Mocking HTTP methods
12. Practical Applications Using the HttpMock with JAI
13. Conclusion and Next Steps
Ok, so let’s get started.
1. Overview
Importance and benefits of unit testing in software engineering
As you know, Unit testing is a best practice software engineering habit. Use it to test individual units of source code, such as functions or methods, to determine whether they meet their design and coding standards. Ensure the quality of your code base by identifying and fixing bugs early in the development process.
Introduction to Mockito and its use in creating mock objects
Mockito is a Java mocking framework that enables you to create mock objects, i.e., simulated versions of real objects. Using Mockito can enhance the quality of your unit tests by reaching nooks and crannies that are hard to reach. We will use Mockito to reduce the tedious, time-consuming integration testing we must do if we didn’t have a way to mock classes.
Introduction to the HttpClient and CompletableFuture in Java 11
The HttpClient is a Java API that you can use to make HTTP requests, and it comes with Java (finally! Woot!). The CompletableFuture is a Java class that represents asynchronous tasks. You use the HttpClient and CompletableFuture together to make asynchronous HTTP requests. JAI, the Java Open AI API Client, uses the HttpClient, and CompletableFutures.
Introduction to JSON and HTTP methods (POST, GET, etc.)
JSON is a simple format for exchanging data, and we use it for request bodies and response bodies, and it has become the sort or de facto standard. Since the Open AI API uses JSON, and I am writing a Java Open AI API Client lib, we use JSON. HTTP is a protocol for transferring hypertext documents, such as web pages. HTTP methods are used to define the actions that can be performed on a resource. Some commonly used HTTP methods include POST, GET, PUT, and DELETE. (We use JParse to parse our JSON for JAI and the examples below. The author of JParse is one of my favorite developers.).
?? Technically, Open AI API does not actually use compliant JSON because some of their payloads use numbers for attribute keys in objects, which is forbidden by the JSON spec, but unless you wrote multiple JSON parsers (streaming, byte array, UTF-8, event-based, recursive descent, index overlay, etc.), you might not know this. ??
2. Setting up the environment
You need to set up your development environment to use Mockito with HttpClient in Java 11 effectively. Here are the steps to follow:
Downloading and setting up Java 11
Download and set up Java 11 using SDKMAN:?SDKMAN is a tool that makes it easy to manage multiple versions of Java on your machine and other frameworks and tools. To download and set up Java 11 using SDKMAN, follow the steps outlined in this tutorial.
Downloading and setting up Java 11 using sdk man
# show list of java candidates from Amazon
% sdk list java | grep 11 | grep amzn
| | 11.0.19 | amzn | | 11.0.19-amzn
| | 11.0.18 | amzn | | 11.0.18-amzn
| | 11.0.17 | amzn | | 11.0.17-amzn
# Install the latest Java 11 from Amazon
% sdk install java 11.0.19-amzn
Brew and Chocolately are excellent, but sdk-man is a must for managing Java compilers and build tools (etc.).
?? If you do a lot of JVM-based projects, then please use SDK Man for maven, Java SDKs, Gradle, etc. Please don’t use other package managers because they are not as up-to-date.
Setting up the IDE and Creating a new Java project
Choose your favorite IDE and install it. For this tutorial, we will use IntelliJ IDEA.
Creating a new Java project
?? I’d like to include other instructions for other IDEs, but I mostly use IDEA. I buy a full license too. It is a great tool if you are a professional developer. It is worth the money.
Adding Mockito to the project via build.gradle or pom.xml
Adding Mockito to the project via build.gradle or pom.xml
If you are using Gradle, add the following dependency to your build.gradle file:
Code Gradle Dependencies
// <https://mvnrepository.com/artifact/org.mockito/mockito-core>
testImplementation 'org.mockito:mockito-core:5.4.0'
If you are using Maven, add the following dependency to your pom.xml file:
Maven pom.xml file Dependencies
<!-- <https://mvnrepository.com/artifact/org.mockito/mockito-core> -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
Once you have added Mockito to your project, you can start writing unit tests.
To use the latest version of Mockito go here and see what the latest version is.
3. Basics of Mockito
How Mockito works
Mockito is a Java testing library that allows developers to create mock objects in automated unit tests for test-driven development (TDD). It simplifies creating mocks by providing easy-to-use interfaces and reducing cognitive burdens.
Mockito uses a simple system of arranging behavior in a three-part "Given-When-Then" style when writing tests:
In summary, Mockito offers a straightforward API to create and configure mock objects, making tests clean, concise, and easy to understand.
Creating and using Mockito mock objects
To create a mock object using Mockito, we use the mock() method from the Mockito library. Here's a basic example:
List mockedList = mock(List.class);
Writing Simple tests with Mockito
In this example, we create a mock object of the List class. You can also specify the class type if you want:
List<String> mockedList = mock(List.class);
After we create a mock object, we can define its behavior. For instance, we can tell the mocked list to return a specific value when a method is invoked:
when(mockedList.size()).thenReturn(10);
In this example, whenever the size() method is invoked on mockedList, it will return 10.
Writing tests with Mockito typically follow the "Arrange, Act, Assert" pattern. Here's a basic example:
import static org.mockito.Mockito.*;
@Test
public void testMockedList() {
// Arrange
List<String> mockedList = mock(List.class);
when(mockedList.size()).thenReturn(10);
// Act
int size = mockedList.size();
// Assert
assertEquals(10, size);
verify(mockedList, times(1)).size();
}
In this test, we:
The verify() method ensures that a method was called on a mock. In this case, we verify that the size() method was called exactly once on the mockedList.
This is a straightforward example. Mockito is robust and provides much more functionalities, such as argument matchers, spying on real objects, and more, but this should serve as a good starting point.
?? Get more information about Mockito
By mastering the basics of Mockito, you can write more efficient and scalable tests for your software. To learn more about Mockito, refer to the following resources:
In summary, Mockito is a framework that simplifies software testing by allowing you to define the output of certain method calls. Here are the key points:
Key Concepts so far:
?? How does Mockito actually work? Do they use reflection? Do they use bytecode generation and special classloaders? No. Not al all! It is all magic and witchcraft. They would likely all burn if this were done during the Salem Witch Trials. Enjoy the magic! While it lasts!
5. Basics of CompletableFuture
How CompletableFuture Works in Java
CompletableFuture is a class in the Java 8+ API that represents asynchronous tasks. It offers additional features compared to Futures, such as the ability to chain tasks and handle errors. CompletableFuture is a Future implementation that provides methods for asynchronous programming in Java. It was introduced in Java 8 and is the evolution of the Java Future API, providing a rich, functional, and fluent API for composing asynchronous computation.
Why and where to use CompletableFuture
CompletableFutures can be used in various situations where you must perform asynchronous tasks. For example, you could use CompletableFutures to make HTTP requests, process database queries, or execute long-running tasks.
Here are some of the benefits of using CompletableFuture:
Examples of using CompletableFuture
Here are some examples of how you could use CompletableFuture:
.
How CompletableFuture Works in Java
CompletableFuture implements both Future and CompletionStage interfaces, extending the capabilities of the traditional Future API by adding a helpful set of operations for composing, combining, and executing tasks asynchronously.
A CompletableFuture object can be seen as a promise of a computation result in the future. Why didn’t they call it a Promise? One thread sets the value for the computation at some point, and other threads can wait for the computation to finish and retrieve the result.
?? Why didn’t they call a CompletableFuture a Promise instead? What is this JavaScript? We can’t call it that?! I knew this guy who wrote Promise lib for Java. He did it before Vert.x did it. But he is a real troll. ??
A simple usage of CompletableFuture would look like this:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Long running task...
return "Result";
});
// ... do some other tasks ...
// get() blocks until the Future completes for testing and integration
String result = future.get();
In this example, CompletableFuture.supplyAsync() is used to create a CompletableFuture, which executes a Supplier asynchronously in a separate thread.
Why and Where to Use CompletableFuture
CompletableFuture is a tool used in modern Java applications to manage long-running tasks that typically block, particularly those involving IO operations like reading files or making network requests. It facilitates non-blocking, responsive applications, making them more efficient and faster.
?? Keep in mind this is long-running in computer speak.
CompletableFuture provides methods for chaining multiple stages of a task pipeline, where each stage executes upon completion of the previous stage. You can also combine multiple CompletableFutures into a single CompletableFuture, which completes when all source futures are complete.
Moreover, CompletableFuture enables exceptional handling capabilities, allowing you to deal with errors at any stage of the asynchronous computation.
In summary, you should use Promises, umm, err, CompletableFuture when executing tasks asynchronously, especially in IO-heavy applications, to make them more responsive and efficient.
?? Is CompletableFuture in your Future?
领英推荐
I’d say it is very Promising! Get it! ??
At least until continuations become mainstream in the VM.
By mastering the basics of CompletableFuture, you can write more efficient and scalable code for asynchronous programming in Java. To learn more about CompletableFuture, please take a look at the following resources. Here are some resources for learning about CompletableFuture in Java:
In summary, CompletableFuture is a Java class representing asynchronous tasks and offering async functionality on top of Futures. It can be used when asynchronous tasks are needed, such as making HTTP requests, processing database queries, or running lengthy tasks. We have explained how CompletableFuture works in Java, where and why to use it, and provided examples. We have also provided guidance on writing code to manage “long-running tasks” like IO operations.
Here are some key concepts to keep in mind about CompletableFuture.
5. Basics of HttpClient in Java 11
Making simple HTTP requests
Finally, Java got its very own HttpClient. It only took what, like, 15 years! The HttpClient class in Java 11 provides a simple way to make HTTP requests. You can use the sendAsync() method to request a simple HTTP. This method takes an HttpRequest object as an argument and returns a CompletableFuture object. The CompletableFuture object will be completed with the response from the HTTP request.
?? While Python and other languages always came with batteries included, sporting needed things like JSON parser and HTTPClients years before Java, you must remember that when Java came out to the world, people were still paying for generic collections libs for C++ back when Java came with Java collections included. (Generics came later). James Gosling is a hero. Java has evolved into a batteries-included platform. It just took a while. I struggled when writing JAI, which HttpClient to use because there are so many good alternatives since Java only recently shipped with a decent one. I. would have picked Vert.x because that is my IO lib of choice. I like OKHttp too. I have used the Apache one on occasion. Initially, I decided to use the built-in one for now. But one day, I will want it to work more seamlessly in the Vert.x environment or Spring, and I might make the HttpClient implementation pluggable, but that is a conversation for another developer notebook.
For example, the following code makes a simple HTTP GET request to the https://www.google.com URL:
Simple HttpClient example
HttpClient httpClient = HttpClient.newBuilder().build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("<https://www.google.com>"))
.build();
CompletableFuture<HttpResponse<String>> future = httpClient.sendAsync(request);
try {
HttpResponse<String> response = future.get();
System.out.println(response.statusCode());
System.out.println(response.body());
} catch (Exception e) {
e.printStackTrace();
}
This code creates a HttpClient object, an HttpRequest object for the https://www.google.com URL, and calls the sendAsync() method on the HttpClient object. The sendAsync() method returns a CompletableFuture object, which is then used to get the response from the HTTP request.
Handling responses
The HttpResponse object returned by the sendAsync() method contains information about the response, such as the status code and the body. The body() method of the HttpResponse object can be used to get the body of the response.
For example, the following code prints the status code and the body of the response from the HTTP GET request made in the previous example:
Using the future
HttpResponse<String> response = future.get();
System.out.println(response.statusCode());
System.out.println(response.body());
This code prints the status code, and the body of the response from the HTTP GET request.
Here is a more extended example taken from our code base (the JAI, the Java Open AI API Client code base).
HttpClient example from JAI that uses send and sendAsync
/**
* Represents a client for interacting with the OpenAI API.
*/
public class OpenAIClient {
private final HttpClient httpClient;
...
/**
* Sends a chat request to the OpenAI API and returns the client response.
*
* @param chatRequest The chat request to be sent.
* @return The client response containing the chat request and the corresponding chat response.
*/
public CompletableFuture<ClientResponse<ChatRequest, ChatResponse>>
chatAsync(final ChatRequest chatRequest) {
final String jsonRequest = ChatRequestSerializer.serialize(chatRequest);
final HttpRequest.Builder requestBuilder =
createRequestBuilderWithBody("/chat/completions")
.POST(HttpRequest.BodyPublishers.ofString(jsonRequest));
final HttpRequest request = requestBuilder.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(
(Function<HttpResponse<String>,
ClientResponse<ChatRequest, ChatResponse>>) response -> {
return getChatRequestChatResponseClientSuccessResponse(
chatRequest, response);
}).exceptionally(e ->
getErrorResponseForChatRequest(e, chatRequest));
}
/**
* Sends a chat request to the OpenAI API and returns the client response.
*
* @param chatRequest The chat request to be sent.
* @return The client response containing the chat request and the corresponding chat response.
*/
public ClientResponse<ChatRequest, ChatResponse>
chat(final ChatRequest chatRequest) {
final String jsonRequest = ChatRequestSerializer.serialize(chatRequest);
final HttpRequest.Builder requestBuilder =
createRequestBuilderWithBody("/chat/completions")
.POST(HttpRequest.BodyPublishers.ofString(jsonRequest));
final HttpRequest request = requestBuilder.build();
try {
final HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
return getChatRequestChatResponseClientSuccessResponse(chatRequest, response);
} catch (Exception e) {
return getErrorResponseForChatRequest(e, chatRequest);
}
}
See OpenAIClient code for more details and an excellent example of using HttpClient.
Then to use the above class, you would do the following:
Create the client
// Create the client
final OpenAIClient client = OpenAIClient.builder().setApiKey(System.getenv("OPEN_AI_KEY")).build();
Create chat request
// Create the chat request
final ChatRequest chatRequest = ChatRequest.builder().setModel("gpt-3.5-turbo")
.addMessage(Message.builder().setContent("What is AI?").setRole(Role.USER).build())
.build();
Invoke the client async for chat
// Call Open AI API with chat message
client.chatAsync(chatRequest).thenAccept(response -> {
response.getResponse().ifPresent(chatResponse ->
chatResponse.getChoices().forEach(System.out::println));
response.getException().ifPresent(Throwable::printStackTrace);
response.getStatusMessage().ifPresent(error ->
System.out.printf("status message %s %d \\n", error,
response.getStatusCode().orElse(0)));
});
Invoke the client synchronously for chat
// Call Open AI API with chat message
final ClientResponse<ChatRequest, ChatResponse> response =
client.chat(chatRequest);
response.getResponse().ifPresent(
chatResponse -> chatResponse.getChoices().forEach(System.out::println));
response.getException().ifPresent(Throwable::printStackTrace);
response.getStatusMessage().ifPresent(
error ->
System.out.printf("status message %s %d \\\\n", error,
response.getStatusCode().orElse(0)))
Above we use the OpenAIClient class to request the OpenAI API using the chat method. The chat method takes a ChatRequest object as an argument and returns a ClientResponse object containing a ChatResponse object and an optional exception. The JAI client uses HttpClient underneath the covers.
The ClientResponse is used to retrieve the ChatResponse, which is used to retrieve a list of Choice objects from the Open AI API. If an exception is present in the ClientResponse object, it is printed to the console. Finally, a status message in the ClientResponse object and the status code are printed to the console.
?? Want to learn more about HttpClient
By mastering the basics of HttpClient in Java 11, you can write more efficient and scalable code for sending HTTP requests and handling responses. To learn more about HttpClient in Java 11, refer to the following resources:
In conclusion, the HttpClient class in Java 11 provides a simple way to request HTTP. You can use the sendAsync() method to request a simple HTTP. This method takes an HttpRequest object as an argument and returns a CompletableFuture object. The CompletableFuture object will be completed with the response from the HTTP request. The HttpResponse object returned by the sendAsync() method contains information about the response, such as the status code and the body. The body() method of the HttpResponse object can be used to get the body of the response. You can use CompletableFuture is used to perform asynchronous tasks, which is perfect for making HTTP requests.
Here are some more concepts about the HttpClient.
7. Creating a Mock HttpClient
Now that we introduced the concepts and did a background on Mockito, HttpClient, and CompletableFuture. Let’s get right down to and create our HttpClientMock.
Creating the HttpClientMock class that extends HttpClient
To create a mock HttpClient, you can create a class that extends the HttpClient class. For example, the following code creates a HttpClientMock class that extends the HttpClient class.
Here is an example of how to use the HttpClientMock class:
Using HttpClientMock sync
HttpClientMock httpClientMock = new HttpClientMock();
// Set up a mocked response for a synchronous POST request.
httpClientMock.setResponsePost("/path/to/resource",
"request body", "response body");
// Make a synchronous POST request.
HttpResponse<String> response = httpClientMock.send(
HttpRequest.newBuilder().uri(URI.create("/path/to/resource"))
.POST(HttpRequest.BodyPublishers.ofString("request body")).build(),
HttpResponse.BodyHandlers.ofString());
// The response will have the body "response body".
System.out.println(response.body());
Using HttpClientMock async
HttpClientMock httpClientMock = new HttpClientMock();
// Set up a mocked response for a synchronous POST request.
httpClientMock.setResponsePostAsync("/path/to/resource",
"request body", "response body");
// Make a synchronous POST request.
HttpResponse<String> response = httpClientMock.sendAsync(
HttpRequest.newBuilder().uri(URI.create("/path/to/resource"))
.POST(HttpRequest.BodyPublishers.ofString("request body")).build(),
HttpResponse.BodyHandlers.ofString()).get();
// The response will have the body "response body".
System.out.println(response.body());
Overriding HttpClient methods (send, sendAsync)
The HttpClientMock class overrides the sendAsync() method. The sendAsync() method is used to make an asynchronous HTTP request. The HttpClientMock class's sendAsync() method's implementation returns the CompletableFuture object configured in the setResponsePost.
This HttpClientMock class is used to create a mocked instance of HttpClient for testing purposes. Here's a step-by-step breakdown of its operations:
Initialization: The constructor HttpClientMock() creates a mocked HttpClient instance, mockClient, using Mockito's mock() function. This instance is used throughout the class to stub its behavior.
public class HttpClientMock extends HttpClient {
private final String apiEndpoint = "<https://api.openai.com/v1/>";
protected HttpClient mockClient;
/**
* Constructor for HttpClientMock, creating a mock HttpClient instance.
*/
public HttpClientMock() {
mockClient = mock(HttpClient.class);
}
Setting up the responses: The class provides several methods for setting up mock responses for different HTTP requests (POST and PUT, both synchronous and asynchronous). These methods (setResponsePost(), setResponsePut(), setResponsePostAsync(), and setResponsePutAsync()) receive the request path, request body, and expected response body as parameters. Inside these methods:
The whole idea here is to make testing our JAI Client, an HttpClient, easier without getting too bogged down in Mockito on every test.
HttpClientMock setResponsePut/Post[Aync] methods
public class HttpClientMock extends HttpClient {
....
/**
* Set up a mocked response for a synchronous POST request.
*
* @param path the request path
* @param requestBody the request body as a String
* @param responseBody the response body as a String
* @return this RequestResponse instance
* @throws Exception in case of errors
*/
public RequestResponse setResponsePost(String path, String requestBody, String responseBody) throws Exception{
final HttpRequest.Builder requestBuilder = createRequestBuilderWithBody(path);
requestBuilder.POST(HttpRequest.BodyPublishers.ofString(requestBody));
final HttpRequest request = requestBuilder.build();
final HttpResponse<String> response = httpResponseBuilder().setBody(responseBody).build();
when(mockClient.send(request, HttpResponse.BodyHandlers.ofString())).thenReturn(response);
return new RequestResponse(request, response);
}
/**
* Set up a mocked response for a asynchronous POST request.
*
* @param path the request path
* @param requestBody the request body as a String
* @param responseBody the response body as a String
* @return this HttpClientMock instance
* @throws Exception in case of errors
*/
public RequestResponse setResponsePostAsync(String path, String requestBody, String responseBody) throws Exception{
final HttpRequest.Builder requestBuilder = createRequestBuilderWithBody(path);
requestBuilder.POST(HttpRequest.BodyPublishers.ofString(requestBody));
final HttpRequest request = requestBuilder.build();
final HttpResponse<String> response = httpResponseBuilder().setBody(responseBody).build();
final CompletableFuture<HttpResponse<String>> future = CompletableFuture.supplyAsync(() -> response);
when(mockClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())).thenReturn(future);
return new RequestResponse(request, response);
}
Helper methods: The createRequestBuilderWithBody() method creates a basic HttpRequest.Builder with some default headers and the given request path. The httpResponseBuilder() method provides a new instance of the inner HttpResponseBuilder class.
HttpClientMock createRequestBuilderWithBody and httpResponseBuilder
public class HttpClientMock extends HttpClient {
....
/**
* Helper method to create a request builder with default headers and given path.
*
* @param path the request path
* @return HttpRequest.Builder instance
*/
private HttpRequest.Builder createRequestBuilderWithBody(final String path) {
return HttpRequest.newBuilder()
.header("Authorization", "Bearer " + "pk-123456789")
.header("Content-Type", "application/json")
.uri(URI.create(apiEndpoint + path));
}
/**
* Get an instance of HttpResponseBuilder.
*
* @return HttpResponseBuilder instance
*/
public static HttpResponseBuilder httpResponseBuilder() {
return new HttpResponseBuilder();
}
HttpResponseBuilder: This is an inner static class for building mock HttpResponse instances. It's a simple builder pattern implementation with some default values. The build() method creates a mock HttpResponse instance and stubs the statusCode() and body() methods to return the set values.
HttpClientMock HttpResponseBuilder
public class HttpClientMock extends HttpClient {
....
/**
* HttpResponseBuilder is a builder for creating mock HttpResponse instances for testing.
*/
public static class HttpResponseBuilder {
private HttpResponseBuilder(){}
private String contentType = "application/json";
private int statusCode = 200;
private String body = "null";
public String getBody() {
return body;
}
public HttpResponseBuilder setBody(String body) {
this.body = body;
return this;
}
public String getContentType() {
return contentType;
}
public HttpResponseBuilder setContentType(String contentType) {
this.contentType = contentType;
return this;
}
public int getStatusCode() {
return statusCode;
}
public HttpResponseBuilder setStatusCode(int statusCode) {
this.statusCode = statusCode;
return this;
}
public HttpResponse<String> build() {
final HttpResponse<String> mockResponse = mock(HttpResponse.class);
when(mockResponse.statusCode()).thenReturn(this.getStatusCode());
when(mockResponse.body()).thenReturn(this.getBody());
return mockResponse;
}
}
Overridden methods: The class overrides all the methods of the HttpClient class to forward the calls to the mockClient. This includes methods related to configurations like connectTimeout(), followRedirects(), proxy(), and others, as well as the send() and sendAsync() methods used for making HTTP requests.
public class HttpClientMock extends HttpClient {
....
/**
* Constructor for HttpClientMock, creating a mock HttpClient instance.
*/
public HttpClientMock() {
mockClient = mock(HttpClient.class);
}
@Override
public Optional<CookieHandler> cookieHandler() {
return mockClient.cookieHandler();
}
@Override
public Optional<Duration> connectTimeout() {
return mockClient.connectTimeout();
}
@Override
public Redirect followRedirects() {
return mockClient.followRedirects();
}
@Override
public Optional<ProxySelector> proxy() {
return mockClient.proxy();
}
@Override
public SSLContext sslContext() {
return mockClient.sslContext();
}
@Override
public SSLParameters sslParameters() {
return mockClient.sslParameters();
}
@Override
public Optional<Authenticator> authenticator() {
return mockClient.authenticator();
}
@Override
public Version version() {
return mockClient.version();
}
@Override
public Optional<Executor> executor() {
return mockClient.executor();
}
@Override
public <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException, InterruptedException {
return mockClient.send(request, responseBodyHandler);
}
@Override
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) {
return mockClient.sendAsync(request, responseBodyHandler);
}
@Override
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler,
HttpResponse.PushPromiseHandler<T> pushPromiseHandler) {
return mockClient.sendAsync(request, responseBodyHandler, pushPromiseHandler);
}
}
Using this class, you can easily set up a mock HttpClient in your tests and define its behavior for specific requests. This is useful when isolating your tests from the actual HTTP layer, for example, to avoid making genuine HTTP requests in unit tests.
Complete code Listing for HttpClientMock that uses Mockito (complete for now)
package com.cloudurable.jai.test.mock;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import java.io.IOException;
import java.net.Authenticator;
import java.net.CookieHandler;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import static org.mockito.Mockito.*;
/**
* HttpClientMock extends HttpClient for creating a mock HttpClient useful for testing.
* It provides a set of methods to set up responses for various request types including synchronous and asynchronous POST and PUT requests.
*/
public class HttpClientMock extends HttpClient {
private final String apiEndpoint = "<https://api.openai.com/v1/>";
protected HttpClient mockClient;
public HttpClient getMock() {
return mockClient;
}
public static class RequestResponse {
private final HttpResponse<String> response;
private final HttpRequest request;
public RequestResponse(HttpRequest request, HttpResponse<String> response) {
this.response = response;
this.request = request;
}
public HttpResponse<String> getResponse() {
return response;
}
public HttpRequest getRequest() {
return request;
}
}
/**
* Set up a mocked response for a synchronous POST request.
*
* @param path the request path
* @param requestBody the request body as a String
* @param responseBody the response body as a String
* @return this RequestResponse instance
* @throws Exception in case of errors
*/
public RequestResponse setResponsePost(String path, String requestBody, String responseBody) throws Exception{
final HttpRequest.Builder requestBuilder = createRequestBuilderWithBody(path);
requestBuilder.POST(HttpRequest.BodyPublishers.ofString(requestBody));
final HttpRequest request = requestBuilder.build();
final HttpResponse<String> response = httpResponseBuilder().setBody(responseBody).build();
when(mockClient.send(request, HttpResponse.BodyHandlers.ofString())).thenReturn(response);
return new RequestResponse(request, response);
}
/**
* Set up a mocked response for a asynchronous POST request.
*
* @param path the request path
* @param requestBody the request body as a String
* @param responseBody the response body as a String
* @return this HttpClientMock instance
* @throws Exception in case of errors
*/
public RequestResponse setResponsePostAsync(String path, String requestBody, String responseBody) throws Exception{
final HttpRequest.Builder requestBuilder = createRequestBuilderWithBody(path);
requestBuilder.POST(HttpRequest.BodyPublishers.ofString(requestBody));
final HttpRequest request = requestBuilder.build();
final HttpResponse<String> response = httpResponseBuilder().setBody(responseBody).build();
final CompletableFuture<HttpResponse<String>> future = CompletableFuture.supplyAsync(() -> response);
when(mockClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())).thenReturn(future);
return new RequestResponse(request, response);
}
/**
* Helper method to create a request builder with default headers and given path.
*
* @param path the request path
* @return HttpRequest.Builder instance
*/
private HttpRequest.Builder createRequestBuilderWithBody(final String path) {
return HttpRequest.newBuilder()
.header("Authorization", "Bearer " + "pk-123456789")
.header("Content-Type", "application/json")
.uri(URI.create(apiEndpoint + path));
}
/**
* Get an instance of HttpResponseBuilder.
*
* @return HttpResponseBuilder instance
*/
public static HttpResponseBuilder httpResponseBuilder() {
return new HttpResponseBuilder();
}
/**
* HttpResponseBuilder is a builder for creating mock HttpResponse instances for testing.
*/
public static class HttpResponseBuilder {
private HttpResponseBuilder(){}
private String contentType = "application/json";
private int statusCode = 200;
private String body = "null";
public String getBody() {
return body;
}
public HttpResponseBuilder setBody(String body) {
this.body = body;
return this;
}
public String getContentType() {
return contentType;
}
public HttpResponseBuilder setContentType(String contentType) {
this.contentType = contentType;
return this;
}
public int getStatusCode() {
return statusCode;
}
public HttpResponseBuilder setStatusCode(int statusCode) {
this.statusCode = statusCode;
return this;
}
public HttpResponse<String> build() {
final HttpResponse<String> mockResponse = mock(HttpResponse.class);
when(mockResponse.statusCode()).thenReturn(this.getStatusCode());
when(mockResponse.body()).thenReturn(this.getBody());
return mockResponse;
}
}
/**
* Constructor for HttpClientMock, creating a mock HttpClient instance.
*/
public HttpClientMock() {
mockClient = mock(HttpClient.class);
}
@Override
public Optional<CookieHandler> cookieHandler() {
return mockClient.cookieHandler();
}
@Override
public Optional<Duration> connectTimeout() {
return mockClient.connectTimeout();
}
@Override
public Redirect followRedirects() {
return mockClient.followRedirects();
}
@Override
public Optional<ProxySelector> proxy() {
return mockClient.proxy();
}
@Override
public SSLContext sslContext() {
return mockClient.sslContext();
}
@Override
public SSLParameters sslParameters() {
return mockClient.sslParameters();
}
@Override
public Optional<Authenticator> authenticator() {
return mockClient.authenticator();
}
@Override
public Version version() {
return mockClient.version();
}
@Override
public Optional<Executor> executor() {
return mockClient.executor();
}
@Override
public <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException, InterruptedException {
return mockClient.send(request, responseBodyHandler);
}
@Override
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) {
return mockClient.sendAsync(request, responseBodyHandler);
}
@Override
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler,
HttpResponse.PushPromiseHandler<T> pushPromiseHandler) {
return mockClient.sendAsync(request, responseBodyHandler, pushPromiseHandler);
}
}
In conclusion, the HttpClientMock class is a valuable tool for creating a mocked instance of HttpClient for testing purposes. It provides methods for setting up mock responses for different HTTP requests, including synchronous and asynchronous POST and PUT requests. By using this class, developers can test their code without making actual HTTP requests, thus reducing the reliance on external APIs, and writing faster unit tests.
Here are some key concepts about HttpClientMock.
Now, let's cover a more realistic example of using HttpClientMock to test OpenAIClient from JAI.
A realistic example of using HttpClientMock to test OpenAIClient for sync calls
package com.cloudurable.jai;
import com.cloudurable.jai.model.ClientResponse;
import com.cloudurable.jai.model.chat.*;
import com.cloudurable.jai.test.mock.HttpClientMock;
import io.nats.jparse.Json;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.net.http.HttpClient;
import java.net.http.HttpResponse;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* Tests for the OpenAIClient.
*/
class ChatClientSyncTest {
String basicChatResponseBody;
String basicChatRequestBody;
ChatRequest basicChatRequest;
/**
* Test method to verify the behavior of the chat method in the OpenAIClient.
* This test mocks a POST request to the /chat/completions endpoint and verifies
* the response from the OpenAIClient chat method.
*
* @throws Exception in case of errors
*/
@Test
void chat() throws Exception {
HttpClientMock httpClientMock;
OpenAIClient client;
httpClientMock = new HttpClientMock();
client = OpenAIClient.builder().setApiKey("pk-123456789").setHttpClient(httpClientMock).build();
//Mock it
final HttpClientMock.RequestResponse requestResponse = httpClientMock.setResponsePost("/chat/completions", basicChatRequestBody, basicChatResponseBody);
final ClientResponse<ChatRequest, ChatResponse> response = client.chat(basicChatRequest);
assertFalse(response.getStatusMessage().isPresent());
assertEquals(200, response.getStatusCode().orElse(-666));
assertTrue(response.getResponse().isPresent());
response.getResponse().ifPresent(chatResponse -> {
assertEquals(1, chatResponse.getChoices().size());
assertEquals("AI stands for artificial intelligence. It refers to the development of computer systems that can perform tasks that typically require human intelligence, such as visual perception, speech recognition, decision-making, and language translation. AI technologies include machine learning, deep learning, natural language processing, and robotics. With AI, machines can learn to recognize patterns in data and make decisions based on that analysis. AI is rapidly advancing and has the potential to significantly impact industries such as healthcare, finance, transportation, and manufacturing.",
chatResponse.getChoices().get(0).getMessage().getContent());
});
HttpClient mock = httpClientMock.getMock();
verify(mock, times(1)).send(requestResponse.getRequest(), HttpResponse.BodyHandlers.ofString());
}
/**
* Setup method to initialize the client, mock HttpClient,
* and set up request and response data before each test.
*/
@BeforeEach
void before() {
// Create the response body
basicChatResponseBody = Json.niceJson("{\\n" +
" 'id': 'chatcmpl-7U7eebdP0zrUDP36wBLvrjCYo7Bkw',\\n" +
" 'object': 'chat.completion',\\n" +
" 'created': 1687413620,\\n" +
" 'model': 'gpt-3.5-turbo-0301',\\n" +
" 'choices': [\\n" +
" {\\n" +
" 'index': 0,\\n" +
" 'message': {\\n" +
" 'role': 'assistant',\\n" +
" 'content': 'AI stands for artificial intelligence. It refers to the development of computer systems that can perform tasks that typically require human intelligence, such as visual perception, speech recognition, decision-making, and language translation. AI technologies include machine learning, deep learning, natural language processing, and robotics. With AI, machines can learn to recognize patterns in data and make decisions based on that analysis. AI is rapidly advancing and has the potential to significantly impact industries such as healthcare, finance, transportation, and manufacturing.'\\n" +
" },\\n" +
" 'finish_reason': 'stop'\\n" +
" }\\n" +
" ],\\n" +
" 'usage': {\\n" +
" 'prompt_tokens': 12,\\n" +
" 'completion_tokens': 97,\\n" +
" 'total_tokens': 109\\n" +
" }\\n" +
"}");
// Create the request body.
basicChatRequest = ChatRequest.builder().setModel("gpt-3.5-turbo")
.addMessage(Message.builder().setContent("What is AI?").setRole(Role.USER).build())
.build();
basicChatRequestBody = ChatRequestSerializer.serialize(basicChatRequest);
}
}
The above example is taken from actually testing JAI, the Java Open AI API Client.
Ok, that is a lot of code. Let’s break it down step by step.
This code is a test class for the ChatClientSync class, which is a good example of using our new HttpClientMock. The purpose of this test class is to verify the behavior of the chat method in the OpenAIClient class.
Let's go through the code step by step:
The ChatClientSyncTest class is declared, which will contain the test methods.
Inside the class, there are several member variables declared:
class ChatClientSyncTest {
String basicChatResponseBody;
String basicChatRequestBody;
ChatRequest basicChatRequest;
The @BeforeEach annotation marks the following method before() as a setup method that will be executed before each test.
The before() method initializes the member variables used in the tests:
/**
* Setup method to initialize the client, mock HttpClient,
* and set up request and response data before each test.
*/
@BeforeEach
void before() {
// Create the response body
basicChatResponseBody = Json.niceJson("{\\n" +
" 'id': 'chatcmpl-7U7eebdP0zrUDP36wBLvrjCYo7Bkw',\\n" +
" 'object': 'chat.completion',\\n" +
" 'created': 1687413620,\\n" +
" 'model': 'gpt-3.5-turbo-0301',\\n" +
" 'choices': [\\n" +
" {\\n" +
" 'index': 0,\\n" +
" 'message': {\\n" +
" 'role': 'assistant',\\n" +
" 'content': 'AI stands for artificial intelligence. It refers to the development of computer systems that can perform tasks that typically require human intelligence, such as visual perception, speech recognition, decision-making, and language translation. AI technologies include machine learning, deep learning, natural language processing, and robotics. With AI, machines can learn to recognize patterns in data and make decisions based on that analysis. AI is rapidly advancing and has the potential to significantly impact industries such as healthcare, finance, transportation, and manufacturing.'\\n" +
" },\\n" +
" 'finish_reason': 'stop'\\n" +
" }\\n" +
" ],\\n" +
" 'usage': {\\n" +
" 'prompt_tokens': 12,\\n" +
" 'completion_tokens': 97,\\n" +
" 'total_tokens': 109\\n" +
" }\\n" +
"}");
// Create the request body.
basicChatRequest = ChatRequest.builder().setModel("gpt-3.5-turbo")
.addMessage(Message.builder().setContent("What is AI?")
.setRole(Role.USER).build())
.build();
basicChatRequestBody = ChatRequestSerializer.serialize(basicChatRequest);
}
The @Test annotation marks the following method chat() as a test method. This method will verify the behavior of the chat method in the OpenAIClient class.
The chat() test method begins by declaring local variables httpClientMock (our new An instance of our new HttpClientMock) and client.
An instance of the HttpClientMock class is created and assigned to the httpClientMock variable. This is our new mock object which simulates the behavior of an HTTP client for testing purposes.
An instance of the OpenAIClient class is created using the OpenAIClient.builder() method, and is passed our new mock. The setApiKey method is called to set an API key, and the setHttpClient method is called to set the previously created httpClientMock as the HTTP client for the OpenAIClient instance. This way OpenAIClient the instance is using our mock instead of trying to make a real REST call.
The httpClientMock object sets up a mock response for a POST request to the "/chat/completions" endpoint. The basicChatRequestBody and basicChatResponseBody variables are passed to the setResponsePost method to define the mock response.
@Test
void chat() throws Exception {
HttpClientMock httpClientMock;
OpenAIClient client;
httpClientMock = new HttpClientMock();
client = OpenAIClient.builder().setApiKey("pk-123456789")
.setHttpClient(httpClientMock).build();
//Mock it
final HttpClientMock.RequestResponse requestResponse =
httpClientMock.setResponsePost("/chat/completions",
basicChatRequestBody, basicChatResponseBody);
The client.chat(basicChatRequest) method is called to invoke the chat API using the OpenAIClient instance. The response is assigned to the response variable.
Various assertions are made to verify the response:
@Test
void chat() throws Exception {
...
assertFalse(response.getStatusMessage().isPresent());
assertEquals(200, response.getStatusCode().orElse(-666));
assertTrue(response.getResponse().isPresent());
response.getResponse().ifPresent(chatResponse -> {
assertEquals(1, chatResponse.getChoices().size());
assertEquals("AI stands for artificial intelligence...",
chatResponse.getChoices().get(0).getMessage().getContent());
});
The httpClientMock.getMock() method is called to retrieve the mock HTTP client object.
The verify method from the Mockito library is used to verify that the send method of the mock HTTP client was called exactly once with the specified request and BodyHandlers.ofString().
@Test
void chat() throws Exception {
...
HttpClient mock = httpClientMock.getMock();
verify(mock, times(1)).send(requestResponse.getRequest(),
HttpResponse.BodyHandlers.ofString());
Here are some key concepts about the test
Ok, now let’s cover testing the async call.
A realistic example of using HttpClientMock to test OpenAIClient for Async calls
package com.cloudurable.jai;
import com.cloudurable.jai.model.ClientResponse;
import com.cloudurable.jai.model.chat.*;
import com.cloudurable.jai.test.mock.HttpClientMock;
import io.nats.jparse.Json;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.net.http.HttpClient;
import java.net.http.HttpResponse;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* Tests for the OpenAIClient.
*/
class ChatClientAsyncTest {
String basicChatResponseBody;
String basicChatRequestBody;
ChatRequest basicChatRequest;
/**
* Setup method to initialize the client, mock HttpClient,
* and set up request and response data before each test.
*/
@BeforeEach
void before() {
// Create the response body
basicChatResponseBody = Json.niceJson("{\\n" +
" 'id': 'chatcmpl-7U7eebdP0zrUDP36wBLvrjCYo7Bkw',\\n" +
" 'object': 'chat.completion',\\n" +
" 'created': 1687413620,\\n" +
" 'model': 'gpt-3.5-turbo-0301',\\n" +
" 'choices': [\\n" +
" {\\n" +
" 'index': 0,\\n" +
" 'message': {\\n" +
" 'role': 'assistant',\\n" +
" 'content': 'AI stands for artificial intelligence. It refers to the development of computer systems that can perform tasks that typically require human intelligence, such as visual perception, speech recognition, decision-making, and language translation. AI technologies include machine learning, deep learning, natural language processing, and robotics. With AI, machines can learn to recognize patterns in data and make decisions based on that analysis. AI is rapidly advancing and has the potential to significantly impact industries such as healthcare, finance, transportation, and manufacturing.'\\n" +
" },\\n" +
" 'finish_reason': 'stop'\\n" +
" }\\n" +
" ],\\n" +
" 'usage': {\\n" +
" 'prompt_tokens': 12,\\n" +
" 'completion_tokens': 97,\\n" +
" 'total_tokens': 109\\n" +
" }\\n" +
"}");
// Create the request body.
basicChatRequest = ChatRequest.builder()
.setModel("gpt-3.5-turbo")
.addMessage(Message.builder()
.setContent("What is AI?")
.setRole(Role.USER).build()
)
.build();
basicChatRequestBody = ChatRequestSerializer.serialize(basicChatRequest);
}
/**
* Test method to verify the behavior of the chatAsync method in the OpenAIClient.
* This test mocks an asynchronous POST request to the /chat/completions endpoint and verifies
* the response from the OpenAIClient chatAsync method.
*
* @throws Exception in case of errors
*/
@Test
void chatAsync() throws Exception {
HttpClientMock httpClientMock;
OpenAIClient client;
httpClientMock = new HttpClientMock();
client = OpenAIClient.builder().setApiKey("pk-123456789")
.setHttpClient(httpClientMock).build();
final HttpClientMock.RequestResponse requestResponse = httpClientMock
.setResponsePostAsync("/chat/completions",
basicChatRequestBody, basicChatResponseBody);
final ClientResponse<ChatRequest, ChatResponse> response = client.chatAsync(basicChatRequest).get();
assertFalse(response.getStatusMessage().isPresent());
assertEquals(200, response.getStatusCode().orElse(-666));
assertTrue(response.getResponse().isPresent());
response.getResponse().ifPresent(chatResponse -> {
assertEquals(1, chatResponse.getChoices().size());
assertEquals("AI stands for artificial intelligence. It refers to the development of computer " +
"systems that can perform tasks that typically require human intelligence, such as visual " +
"perception, speech recognition, decision-making, and language translation. AI technologies " +
"include machine learning, deep learning, natural language processing, and robotics." +
" With AI, machines can learn to recognize patterns in data and make decisions based " +
"on that analysis. AI is rapidly advancing and has the potential to significantly " +
"impact industries such as healthcare, finance, transportation, and manufacturing.",
chatResponse.getChoices().get(0).getMessage().getContent());
});
HttpClient mock = httpClientMock.getMock();
verify(mock, times(1))
.sendAsync(requestResponse.getRequest(),
HttpResponse.BodyHandlers.ofString());
}
}
The above is also a real-world example taken from testing JAI, the Java Open AI API Client.
This should seem familiar by now.
?? Ask yourself: When writing tests with Mockito, did we use the "Given-When-Then" style? First, set up the test by creating mock objects and defining behavior with stub methods. Then, invoke the method being tested on the class under test. Finally, assert the results. Did we do this for this test? See if you can identify each step.
Conclusion
Remember the points we covered:
In this developer notebook, we covered several topics related to testing with Mockito and asynchronous code. We began with an introduction to the Mockito framework and the importance of testing asynchronous code. We then discussed setting up the test environment, including an overview of the ChatClientSyncTest and ChatClientAsyncTest classes, an explanation of the @BeforeEach and @Test annotations, and a description of the before() method.
Next, we covered several important concepts related to testing, including CompletableFutures and the built-in Java HttpClient. We also discussed testing synchronous calls with Mockito, including an explanation of the chat() method, the declaration of local variables, the creation of the HttpClientMock object, and the verification of the response.
Finally, we covered testing asynchronous calls with Mockito, including an explanation of the chatAsync() method, the declaration of local variables, the creation of the HttpClientMock object, and the verification of the response.
Following the examples and explanations in this article, you should now understand how to use Mockito to test synchronous and asynchronous code and set up a test environment for your applications.
?? Remember: When writing tests with Mockito, you can arrange behavior in a "Given-When-Then" style. First, set up the system for the test by creating mock objects and defining behavior with stub methods. Then, invoke the method being tested on the class under test. Finally, assert that specific results have occurred.
More content from the author