This guide covers the comprehensive integration testing strategy for the Firefly Common Client Library, including WireMock for REST/SOAP services, in-process gRPC servers, and best practices for testing all client types.
- Overview
- Testing Infrastructure
- REST Client Integration Tests
- gRPC Client Integration Tests
- SOAP Client Integration Tests
- Helper Integration Tests
- Best Practices
- Running Tests
The library includes comprehensive integration tests that verify all client types work correctly with real (mocked) services:
| Client Type | Testing Framework | Test Coverage |
|---|---|---|
| REST | WireMock | ✅ Full coverage |
| gRPC | In-Process Server | ✅ Full coverage |
| SOAP | WireMock | ✅ Full coverage |
| GraphQL Helper | Unit Tests | ✅ Full coverage |
| OAuth2 Helper | Unit Tests | ✅ Full coverage |
| Multipart Helper | Unit Tests | ✅ Full coverage |
| WebSocket Helper | Unit Tests | ✅ Full coverage |
WireMock is used to mock HTTP-based services (REST and SOAP):
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>3.3.1</version>
<scope>test</scope>
</dependency>Key Features:
- Dynamic port allocation
- Request matching and verification
- Scenario-based testing (for retry logic)
- SOAP/XML support
- Delay simulation for timeout testing
For gRPC testing, we use in-process servers:
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-testing</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-inprocess</artifactId>
<scope>test</scope>
</dependency>Key Features:
- No network overhead
- Fast test execution
- Full gRPC feature support
- Streaming support
src/test/java/org/fireflyframework/common/client/rest/RestClientIntegrationTest.java
@Test
@DisplayName("Should perform GET request successfully")
void shouldPerformGetRequestSuccessfully() {
// Given: Mock REST endpoint
wireMockServer.stubFor(get(urlEqualTo("/users/123"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"id\":\"123\",\"name\":\"John Doe\"}")));
// When: Execute GET request
Mono<User> result = client.get("/users/{id}", User.class)
.pathVariable("id", "123")
.execute();
// Then: Verify response
StepVerifier.create(result)
.assertNext(user -> {
assertThat(user.id()).isEqualTo("123");
assertThat(user.name()).isEqualTo("John Doe");
})
.verifyComplete();
}@Test
@DisplayName("Should handle 404 errors gracefully")
void shouldHandle404ErrorsGracefully() {
wireMockServer.stubFor(get(urlEqualTo("/users/999"))
.willReturn(aResponse()
.withStatus(404)
.withBody("{\"error\":\"User not found\"}")));
Mono<User> result = client.get("/users/{id}", User.class)
.pathVariable("id", "999")
.execute();
StepVerifier.create(result)
.expectError(ServiceNotFoundException.class)
.verify();
}@Test
@DisplayName("Should retry on 500 errors")
void shouldRetryOn500Errors() {
// First 2 calls fail, third succeeds
wireMockServer.stubFor(get(urlEqualTo("/api/data"))
.inScenario("Retry Scenario")
.whenScenarioStateIs("Started")
.willReturn(aResponse().withStatus(500))
.willSetStateTo("First Retry"));
wireMockServer.stubFor(get(urlEqualTo("/api/data"))
.inScenario("Retry Scenario")
.whenScenarioStateIs("First Retry")
.willReturn(aResponse().withStatus(500))
.willSetStateTo("Second Retry"));
wireMockServer.stubFor(get(urlEqualTo("/api/data"))
.inScenario("Retry Scenario")
.whenScenarioStateIs("Second Retry")
.willReturn(aResponse()
.withStatus(200)
.withBody("{\"data\":\"success\"}")));
// Verify retry succeeded
StepVerifier.create(client.get("/api/data", String.class).execute())
.expectNextCount(1)
.verifyComplete();
// Verify 3 attempts were made
verify(exactly(3), getRequestedFor(urlEqualTo("/api/data")));
}@Test
@DisplayName("Should timeout on slow responses")
void shouldTimeoutOnSlowResponses() {
wireMockServer.stubFor(get(urlEqualTo("/api/slow"))
.willReturn(aResponse()
.withStatus(200)
.withFixedDelay(5000) // 5 second delay
.withBody("{\"data\":\"slow\"}")));
Mono<String> result = client.get("/api/slow", String.class)
.timeout(Duration.ofSeconds(1))
.execute();
StepVerifier.create(result)
.expectError(TimeoutException.class)
.verify();
}mvn test -Dtest=RestClientIntegrationTestsrc/test/java/org/fireflyframework/common/client/grpc/GrpcClientIntegrationTest.java
@Test
@DisplayName("Should execute unary RPC successfully")
void shouldExecuteUnaryRpcSuccessfully() {
// Given: In-process gRPC server
TestServiceImpl testService = new TestServiceImpl();
server = InProcessServerBuilder
.forName(SERVER_NAME)
.directExecutor()
.addService(testService)
.build()
.start();
// When: Execute unary RPC
var result = grpcClient.executeUnary(
channel,
TestServiceGrpc.newStub(channel)::sayHello,
HelloRequest.newBuilder().setName("World").build()
);
// Then: Verify response
StepVerifier.create(result)
.assertNext(response -> {
assertThat(response.getMessage()).isEqualTo("Hello, World!");
})
.verifyComplete();
}@Test
@DisplayName("Should handle server streaming RPC")
void shouldHandleServerStreamingRpc() {
var result = grpcClient.executeServerStreaming(
channel,
TestServiceGrpc.newStub(channel)::streamNumbers,
NumberRequest.newBuilder().setCount(5).build()
);
StepVerifier.create(result)
.expectNextCount(5)
.verifyComplete();
}@Test
@DisplayName("Should handle gRPC errors gracefully")
void shouldHandleGrpcErrorsGracefully() {
var result = grpcClient.executeUnary(
channel,
TestServiceGrpc.newStub(channel)::failingMethod,
EmptyRequest.newBuilder().build()
);
StepVerifier.create(result)
.expectErrorSatisfies(throwable -> {
assertThat(throwable).isInstanceOf(StatusRuntimeException.class);
StatusRuntimeException ex = (StatusRuntimeException) throwable;
assertThat(ex.getStatus().getCode()).isEqualTo(Status.Code.INTERNAL);
})
.verify();
}mvn test -Dtest=GrpcClientIntegrationTestsrc/test/java/org/fireflyframework/common/client/soap/SoapClientIntegrationTest.java
@Test
@DisplayName("Should invoke SOAP operation successfully")
void shouldInvokeSoapOperationSuccessfully() {
// Given: Mock WSDL and SOAP response
String soapResponse = """
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns:AddResponse xmlns:ns="http://tempuri.org/">
<AddResult>8</AddResult>
</ns:AddResponse>
</soap:Body>
</soap:Envelope>
""";
wireMockServer.stubFor(post(urlEqualTo("/calculator.asmx"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "text/xml; charset=utf-8")
.withBody(soapResponse)));
// When: Invoke SOAP operation
var result = soapClient.invoke("Add", request);
// Then: Verify response
StepVerifier.create(result)
.assertNext(response -> {
assertThat(response.getResult()).isEqualTo(8);
})
.verifyComplete();
}@Test
@DisplayName("Should handle WSDL with authentication parameters")
void shouldHandleWsdlWithAuthenticationParameters() {
String wsdlUrl = baseUrl + "/service.asmx?WSDL&user=admin&password=secret";
var soapClient = new SoapClientBuilder()
.wsdlUrl(wsdlUrl)
.build();
assertThat(soapClient).isNotNull();
}mvn test -Dtest=SoapClientIntegrationTestUnit Tests Location: src/test/java/org/fireflyframework/common/client/helpers/GraphQLClientHelperTest.java
Coverage:
- ✅ Query execution
- ✅ Mutation execution
- ✅ Variable handling
- ✅ Error handling
- ✅ Retry logic
- ✅ Query caching
- ✅ Batch operations
Unit Tests Location: src/test/java/org/fireflyframework/common/client/helpers/OAuth2ClientHelperTest.java
Coverage:
- ✅ Token acquisition
- ✅ Token caching
- ✅ Multi-scope support
- ✅ Refresh token flow
- ✅ Retry logic
- ✅ Error handling
Unit Tests Location: src/test/java/org/fireflyframework/common/client/helpers/MultipartUploadHelperTest.java
Coverage:
- ✅ File upload
- ✅ Progress tracking
- ✅ Chunked uploads
- ✅ Parallel uploads
- ✅ File validation
- ✅ Compression
- ✅ Retry logic
Unit Tests Location: src/test/java/org/fireflyframework/common/client/helpers/WebSocketClientHelperTest.java
Coverage:
- ✅ Connection management
- ✅ Automatic reconnection
- ✅ Heartbeat/ping-pong
- ✅ Message queuing
- ✅ Connection pooling
- ✅ Message acknowledgment
- ✅ Binary messages
@BeforeAll
static void startWireMock() {
wireMockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort());
wireMockServer.start();
WireMock.configureFor("localhost", wireMockServer.port());
}
@AfterAll
static void stopWireMock() {
if (wireMockServer != null) {
wireMockServer.stop();
}
}
@BeforeEach
void resetWireMock() {
wireMockServer.resetAll();
}wireMockServer.stubFor(post(urlEqualTo("/api/endpoint"))
.inScenario("Retry Scenario")
.whenScenarioStateIs("Started")
.willReturn(aResponse().withStatus(500))
.willSetStateTo("First Retry"));verify(exactly(1), postRequestedFor(urlEqualTo("/api/endpoint"))
.withHeader("Authorization", equalTo("Bearer token"))
.withRequestBody(containing("expected-data")));StepVerifier.create(result)
.assertNext(response -> {
assertThat(response.getData()).isNotNull();
})
.verifyComplete();@Test
@DisplayName("Should handle network errors")
void shouldHandleNetworkErrors() {
wireMockServer.stubFor(get(urlEqualTo("/api/data"))
.willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER)));
StepVerifier.create(client.get("/api/data", String.class).execute())
.expectError(ServiceClientException.class)
.verify();
}mvn clean testmvn test -Dtest=RestClientIntegrationTest
mvn test -Dtest=GrpcClientIntegrationTest
mvn test -Dtest=SoapClientIntegrationTestmvn test -Dtest=RestClientIntegrationTest#shouldPerformGetRequestSuccessfullymvn clean test jacoco:reportCoverage report will be available at: target/site/jacoco/index.html
mvn test -T 4| Test Suite | Tests | Coverage |
|---|---|---|
| REST Client Integration | 14 tests | ✅ 100% |
| gRPC Client Integration | 7 tests | ✅ 100% |
| SOAP Client Integration | 8 tests | ✅ 100% |
| GraphQL Helper | 11 tests | ✅ 100% |
| OAuth2 Helper | 17 tests | ✅ 100% |
| Multipart Helper | 16 tests | ✅ 100% |
| WebSocket Helper | 20 tests | ✅ 100% |
| Total | 280+ tests | ✅ 100% |
name: Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
- name: Run Integration Tests
run: mvn clean test
- name: Generate Coverage Report
run: mvn jacoco:report
- name: Upload Coverage
uses: codecov/codecov-action@v3If you encounter port conflicts, WireMock uses dynamic ports by default:
wireMockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort());Ensure unique server names for parallel tests:
String serverName = "test-server-" + UUID.randomUUID();
server = InProcessServerBuilder.forName(serverName).build().start();Increase timeout for slow tests:
StepVerifier.create(result)
.expectNextCount(1)
.verifyComplete(Duration.ofSeconds(30));