Internal testing utilities for OpenAI integration with Quarkus LangChain4j
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Internal testing utilities for the Quarkus LangChain4j OpenAI integration. This package provides WireMock-based test infrastructure that enables developers to write integration tests for OpenAI API interactions without making actual API calls. It includes a base test class with assertion helpers, a response transformer for dynamic test configuration, and pre-configured WireMock mappings for common OpenAI endpoints.
pom.xml in test scope:<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-openai-testing-internal</artifactId>
<version>1.7.4</version>
<scope>test</scope>
</dependency>import io.quarkiverse.langchain4j.openai.testing.internal.OpenAiBaseTest;
import io.quarkiverse.langchain4j.openai.testing.internal.ChatCompletionTransformer;
import io.quarkiverse.langchain4j.openai.testing.internal.OpenAiBaseTest.MessageContent;import io.quarkiverse.langchain4j.openai.testing.internal.OpenAiBaseTest;
import org.junit.jupiter.api.Test;
import java.util.Map;
public class MyOpenAiTest extends OpenAiBaseTest {
@Test
public void testChatCompletion() throws Exception {
// Set expected chat completion response
setChatCompletionMessageContent("Hello from OpenAI!");
// Execute your test code that calls OpenAI API
// The WireMock server will intercept requests
// Validate the request that was sent
Map<String, Object> requestAsMap = getRequestAsMap();
assertSingleRequestMessage(requestAsMap, "Test prompt");
}
}This testing library is built on WireMock and provides three main components:
Tests extend OpenAiBaseTest to inherit WireMock infrastructure and assertion helpers. The ChatCompletionTransformer automatically loads as a WireMock extension and transforms responses based on test configuration. Pre-configured stub mappings for /v1/chat/completions and /v1/images/generations endpoints are automatically loaded from the classpath.
OpenAiBaseTest provides the foundation for OpenAI integration tests, extending WiremockAware with OpenAI-specific assertion utilities.
public abstract class OpenAiBaseTest extends WiremockAware {
/**
* Jackson ObjectMapper instance for JSON processing.
* Initialized by beforeAll() lifecycle method.
* Can be used by test subclasses for custom JSON operations.
*/
static ObjectMapper mapper;
/**
* JUnit 5 lifecycle method that initializes Jackson ObjectMapper for JSON processing.
* Annotated with @BeforeAll and called automatically before all tests in the class.
*/
@BeforeAll
static void beforeAll();
/**
* Sets the expected chat completion message content for the current test.
* This content will be injected into WireMock responses via ChatCompletionTransformer.
*
* @param content The message content to return in chat completion responses
*/
protected void setChatCompletionMessageContent(String content);
/**
* JUnit 5 lifecycle method that automatically clears chat completion message content
* after each test. Annotated with @AfterEach and called automatically.
*/
@AfterEach
public void restoreOriginalChatCompletionMessageContent();
/**
* Converts the single logged request body to a Map for assertion purposes.
* Uses Jackson ObjectMapper for JSON deserialization.
*
* @return The request body as a Map<String, Object>
* @throws IOException If JSON parsing fails
*/
protected Map<String, Object> getRequestAsMap() throws IOException;
/**
* Converts raw request body bytes to a Map for assertion purposes.
* Uses Jackson ObjectMapper for JSON deserialization.
*
* @param body The raw request body bytes
* @return The request body as a Map<String, Object>
* @throws IOException If JSON parsing fails
*/
protected Map<String, Object> getRequestAsMap(byte[] body) throws IOException;
}Validate chat completion request message structures.
/**
* Asserts that the request contains a single message with role "user"
* and the specified content value.
*
* @param requestAsMap The parsed request body as a Map
* @param value The expected message content
*/
protected static void assertSingleRequestMessage(
Map<String, Object> requestAsMap,
String value
);
/**
* Asserts that the request contains multiple messages matching the provided list.
* Each MessageContent record specifies the expected role and content for a message.
*
* @param requestAsMap The parsed request body as a Map
* @param messageContents List of expected messages with their roles and content
*/
protected static void assertMultipleRequestMessage(
Map<String, Object> requestAsMap,
List<MessageContent> messageContents
);
/**
* Generic assertion helper for custom message validations.
* Provides access to the messages array for custom assertion logic.
*
* @param requestAsMap The parsed request body as a Map
* @param messagesAssertions Consumer function to perform custom assertions on messages
*/
protected static void assertMessages(
Map<String, Object> requestAsMap,
Consumer<List<? extends Map>> messagesAssertions
);Validate function/tool call structures in chat completion requests.
/**
* Asserts that the request contains a single function/tool with the specified name.
*
* @param requestAsMap The parsed request body as a Map
* @param functionName The expected function/tool name
*/
protected static void assertSingleFunction(
Map<String, Object> requestAsMap,
String functionName
);
/**
* Generic assertion helper for custom function/tool validations.
* Provides access to the tools/functions array for custom assertion logic.
*
* @param requestAsMap The parsed request body as a Map
* @param functionAssertions Consumer function to perform custom assertions on functions
*/
protected static void assertFunctions(
Map<String, Object> requestAsMap,
Consumer<List<? extends Map>> functionAssertions
);OpenAiBaseTest inherits WireMock testing infrastructure from WiremockAware.
/**
* Returns WireMock URL template for configuration properties.
* Template: "http://localhost:${quarkus.wiremock.devservices.port}"
*
* @return URL template string for use in configuration
*/
public static String wiremockUrlForConfig();
/**
* Returns WireMock URL with path template for configuration properties.
*
* @param path The path to append to the URL template
* @return URL template string with path for use in configuration
*/
public static String wiremockUrlForConfig(String path);
/**
* Returns the resolved WireMock URL at runtime.
*
* @return The actual WireMock URL with resolved port
*/
public String resolvedWiremockUrl();
/**
* Returns the resolved WireMock URL with path at runtime.
*
* @param path The path to append to the URL
* @return The actual WireMock URL with path
*/
public String resolvedWiremockUrl(String path);
/**
* Gets or creates a WireMock client instance.
*
* @return WireMock client for interacting with the mock server
*/
protected WireMock wiremock();
/**
* Gets or creates a WireMock client instance for a specific port.
*
* @param port The port number for the WireMock instance
* @return WireMock client for the specified port
*/
protected WireMock wiremock(Integer port);
/**
* Gets the resolved WireMock port from configuration.
*
* @return The port number WireMock is running on
*/
protected Integer getResolvedWiremockPort();
/**
* Clears all logged requests in WireMock.
* Useful for cleaning up between test scenarios.
*/
protected void resetRequests();
/**
* Clears all stub mappings in WireMock.
* Useful for removing default stubs and configuring custom behavior.
*/
protected void resetMappings();
/**
* Gets the single logged request, asserting that exactly one request exists.
*
* @return The single logged request
* @throws AssertionError if the number of requests is not exactly 1
*/
protected LoggedRequest singleLoggedRequest();
/**
* Gets the single logged request from a specific WireMock instance.
*
* @param wireMock The WireMock client instance to query
* @return The single logged request
* @throws AssertionError if the number of requests is not exactly 1
*/
protected LoggedRequest singleLoggedRequest(WireMock wireMock);
/**
* Gets the request body of the single logged request.
*
* @return The request body as a byte array
* @throws AssertionError if the number of requests is not exactly 1
*/
protected byte[] requestBodyOfSingleRequest();
/**
* Gets request body from a ServeEvent.
*
* @param serveEvent The serve event containing request information
* @return The request body as a byte array
*/
protected byte[] getRequestBody(ServeEvent serveEvent);Dynamic response transformation for chat completion tests.
public class ChatCompletionTransformer implements ResponseDefinitionTransformerV2 {
/**
* Creates a new ChatCompletionTransformer instance.
* This constructor is called automatically by WireMock's extension loading mechanism
* via service loader (META-INF/services).
*/
public ChatCompletionTransformer();
/**
* Transforms response definitions by injecting chat completion message content
* if the system property is set. This method is called automatically by WireMock
* for responses configured to use the "completion-transformer".
*
* @param serveEvent The serve event containing request and response information
* @return The transformed response definition
*/
public ResponseDefinition transform(ServeEvent serveEvent);
/**
* Returns the transformer name used to reference this transformer in stub mappings.
*
* @return "completion-transformer"
*/
public String getName();
/**
* Sets the chat completion message content via system property.
* The content will be injected into responses by the transformer.
* This is typically called from test code or via OpenAiBaseTest.setChatCompletionMessageContent().
*
* @param content The message content to inject into responses
*/
public static void setContent(String content);
/**
* Clears the chat completion message content by removing the system property.
* This is typically called from test cleanup code.
*/
public static void clearContent();
}The transformer is automatically registered as a WireMock Extension via service loader mechanism (META-INF/services/com.github.tomakehurst.wiremock.extension.Extension).
Automatic configuration of WireMock DevServices for OpenAI testing.
public class WiremockConfigBuilderCustomizer implements SmallRyeConfigBuilderCustomizer {
/**
* Creates a new WiremockConfigBuilderCustomizer instance.
* This constructor is called automatically during Quarkus configuration phase
* via service loader (META-INF/services).
*/
public WiremockConfigBuilderCustomizer();
/**
* Configures WireMock DevServices with OpenAI-specific settings.
* This method is called automatically during Quarkus configuration phase.
*
* Sets the following properties:
* - quarkus.wiremock.devservices.global-response-templating: "true"
* - quarkus.wiremock.devservices.extension-scanning-enabled: "true"
* - quarkus.wiremock.devservices.files-mapping: "classpath:/openai"
*
* Priority: 500
*
* @param builder The SmallRye configuration builder
*/
public void configBuilder(final SmallRyeConfigBuilder builder);
}The customizer is automatically registered via service loader mechanism (META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer).
Record type for specifying expected message content in assertions.
public static record MessageContent(String role, String content) {
/**
* Creates a MessageContent record with role and content.
*
* @param role The role of the message (e.g., "user", "assistant", "system", "function")
* @param content The message content (can be null)
*/
}Usage example:
List<MessageContent> expected = List.of(
new MessageContent("system", "You are a helpful assistant"),
new MessageContent("user", "What is the weather?"),
new MessageContent("assistant", "I'll help you check the weather")
);
assertMultipleRequestMessage(requestAsMap, expected);Types from external libraries used in the public API:
// WireMock types
import com.github.tomakehurst.wiremock.extension.ResponseDefinitionTransformerV2;
import com.github.tomakehurst.wiremock.stubbing.ServeEvent;
import com.github.tomakehurst.wiremock.http.ResponseDefinition;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.verification.LoggedRequest;
// SmallRye Config types
import io.smallrye.config.SmallRyeConfigBuilder;
import io.smallrye.config.SmallRyeConfigBuilderCustomizer;
// Jackson types
import wiremock.com.fasterxml.jackson.databind.ObjectMapper;
// Standard Java types
import java.util.Map;
import java.util.List;
import java.util.function.Consumer;
import java.io.IOException;The package includes WireMock stub mappings in /openai/mappings/completions.json that are automatically loaded:
Endpoint: POST /v1/chat/completions
Request Validation: Validates the presence and structure of:
messages array with role and content fieldsmax_tokens, temperature, top_p, n, stream, frequency_penalty, presence_penalty, logit_bias, userResponse: Returns a chat completion response with the content set via ChatCompletionTransformer. The response uses the template variable {{parameters.ChatCompletionMessageContent}} which is populated from the system property.
Example response structure:
{
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1677652288,
"model": "gpt-3.5-turbo",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "{{parameters.ChatCompletionMessageContent}}"
},
"finish_reason": "stop"
}]
}Endpoint: POST /v1/images/generations
Request Validation: Validates the presence and structure of:
model (string)prompt (string)n (integer, optional)size (string, optional)quality (string, optional)style (string, optional)response_format (string, optional)Response: Returns a sample image generation response with a placeholder URL and revised prompt.
Example response structure:
{
"created": 1677652288,
"data": [{
"url": "https://example.com/image.png",
"revised_prompt": "A revised version of the prompt"
}]
}@Test
public void testMultipleMessages() throws Exception {
setChatCompletionMessageContent("I can help with that!");
// Execute test code
// Validate request had multiple messages
Map<String, Object> requestAsMap = getRequestAsMap();
List<MessageContent> expected = List.of(
new MessageContent("system", "You are a helpful assistant"),
new MessageContent("user", "Help me with Java")
);
assertMultipleRequestMessage(requestAsMap, expected);
}@Test
public void testFunctionCall() throws Exception {
setChatCompletionMessageContent("Calling function...");
// Execute test code that includes function calling
// Validate request included the expected function
Map<String, Object> requestAsMap = getRequestAsMap();
assertSingleFunction(requestAsMap, "get_weather");
}@Test
public void testCustomMessageValidation() throws Exception {
setChatCompletionMessageContent("Response content");
// Execute test code
Map<String, Object> requestAsMap = getRequestAsMap();
assertMessages(requestAsMap, messages -> {
assertThat(messages).hasSizeGreaterThan(1);
assertThat(messages.get(0)).containsEntry("role", "system");
});
}@Test
public void testCustomFunctionValidation() throws Exception {
setChatCompletionMessageContent("Function result");
// Execute test code
Map<String, Object> requestAsMap = getRequestAsMap();
assertFunctions(requestAsMap, functions -> {
assertThat(functions).hasSize(2);
assertThat(functions.get(0))
.containsEntry("type", "function")
.extracting("function")
.asInstanceOf(MAP_STRING_OBJECT)
.containsEntry("name", "get_weather");
});
}@Test
public void testWithDirectWireMockAccess() throws Exception {
// Reset any existing stubs
resetMappings();
// Configure custom stub
wiremock().register(
post(urlEqualTo("/v1/chat/completions"))
.willReturn(aResponse()
.withStatus(200)
.withBody("{\"custom\": \"response\"}"))
);
// Execute test code
// Get logged request
LoggedRequest request = singleLoggedRequest();
assertThat(request.getUrl()).isEqualTo("/v1/chat/completions");
// Clean up
resetRequests();
}@TestConfiguration
public class TestConfig {
@Bean
public OpenAIClient openAIClient() {
// Use WireMock URL in configuration
String baseUrl = OpenAiBaseTest.wiremockUrlForConfig();
// Configure client with template URL
// At runtime, this will resolve to actual WireMock port
return OpenAIClient.builder()
.baseUrl(baseUrl)
.build();
}
}This package requires the following dependencies:
Core Testing Infrastructure:
io.quarkiverse.langchain4j:quarkus-langchain4j-testing-internal (v1.7.4) - Parent testing utilities providing WiremockAware base classWireMock:
org.wiremock:wiremock-standalone - HTTP mocking framework for intercepting OpenAI API callsRuntime Dependencies (transitive):
org.junit.jupiter:junit-jupiter-api)org.assertj:assertj-core)com.fasterxml.jackson.core:jackson-databind)io.smallrye.config:smallrye-config)<maven.deploy.skip>true</maven.deploy.skip>, intended solely for use within the Quarkus LangChain4j project's test suitesclasspath:/openai automatically by WiremockConfigBuilderCustomizerChatCompletionTransformer uses system properties for configuration, allowing test-specific response content without modifying stub mappingsrestoreOriginalChatCompletionMessageContent()) is automatic via JUnit 5 @AfterEach lifecycle