StableMock is a JUnit 5 extension that automatically records third-party API calls and replays them using WireMock — without manual setup, Docker, or brittle matchers.
Perfect for mocking external/third-party APIs • Fast, lightweight HTTP mocking without Docker overhead • Tests with dynamic request data that need intelligent pattern matching
Compare StableMock against WireMock 3 and WireMock Cloud. See why developers choose the open-source solution that works out of the box.
Zero-config recording • Smart pattern matching • Free forever
| Feature | StableMock | WireMock 3 | WireMock Cloud |
|---|---|---|---|
| Setup & Recording | ✅ *Zero-Config Recording* — just annotate & go | ⚠️ Manual setup required | ✅ SaaS recording & cloud UI |
| Dynamic Data Handling | ✅ *Smart Patterns* that learn what changes | ❌ Manual matchers needed | ✅ Yes (dynamic templates, stateful mocks) |
| JUnit 5 Integration | ✅ Native annotation, seamless lifecycle | ⚠️ Requires manual lifecycle handling | ✅ Native integration + enterprise CI support |
| Nested JSON/XML Support | ✅ Intuitive DSL (e.g., json:user.session.token) | ❌ Custom JSONPath/XPath required | ✅ Yes (supports complex formats) |
| GraphQL Support | ✅ Auto-detected, built-in support | ❌ Manual config only | ✅ Yes — full GraphQL/gRPC support |
| Scenarios / Sequential Responses | ✅ Built-in support for flows & sequences | ✅ Possible but heavy manual setup | ✅ Fully managed scenario/state machine flows |
| Auto-Detect Dynamic Fields | ✅ Detects & ignores fluctuating fields automatically | ❌ No built-in detection/learning | ⚠️ Offers advanced automation, but SaaS-only |
| Playback Reliability | ✅ Stable reproducibility — record once, run anywhere | ⚠️ Depends on custom mappings | ⚠️ Cloud-first, offline may be limited |
| Offline & CI/CD Ready | ✅ Local files + dedicated Gradle/CI tasks | ✅ Local use supported | ⚠️ Primarily cloud-based; offline workflows limited |
| Free & Open Source | ✅ MIT License — fully free, unrestricted | ✅ Apache 2.0 — open source | ❌ SaaS model (free tier exists) |
WireMock Cloud features listed for comparison only.
This test runs twice automatically:
StableMock compares the two runs to detect which fields change between executions, then automatically creates ignore patterns so your tests remain stable even when request data varies.
@U(urls = { "https://api1.com", "https://api2.com" },
properties = { "app.api1.url", "app.api2.url" })
@SpringBootTest
class MyTest extends BaseStableMockTest {
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
autoRegisterProperties(registry, MyTest.class);
}
@Test
public void myTest() {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/api/users"))
.GET()
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString()
);
assertEquals(200, response.statusCode());
}
} That's it. No configuration. No setup. Just works. Even with everchanging request attributes.
Use multiple @U annotations in the same test method with different URLs, scenarios, and ignore patterns.
@U(urls = "https://api1.example.com", scenario = true, ignore = { "Date", "Connection" })
@U(urls = "https://api2.example.com", ignore = { "json:timestamp", "json:requestId" })
@SpringBootTest
class AdvancedTest extends BaseStableMockTest {
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
autoRegisterProperties(registry, AdvancedTest.class);
}
@Test
public void testMultipleServices() {
HttpClient client = HttpClient.newHttpClient();
String baseUrl = "http://localhost:8080";
// Scenario mode: Sequential responses from api1
// First call returns "pending"
HttpRequest req1 = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/api1/job/status"))
.GET()
.build();
HttpResponse<String> resp1 = client.send(req1, HttpResponse.BodyHandlers.ofString());
// Second call returns "processing" (different response)
HttpResponse<String> resp2 = client.send(req1, HttpResponse.BodyHandlers.ofString());
// Explicit ignore patterns: api2 ignores dynamic fields
String body = String.format(
"{"action":"create","timestamp":%d,"requestId":"%s"}",
System.currentTimeMillis(),
UUID.randomUUID().toString()
);
HttpRequest req2 = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/api2/users"))
.POST(HttpRequest.BodyPublishers.ofString(body))
.header("Content-Type", "application/json")
.build();
HttpResponse<String> resp3 = client.send(req2, HttpResponse.BodyHandlers.ofString());
assertEquals(200, resp1.statusCode());
assertEquals(200, resp2.statusCode());
assertEquals(200, resp3.statusCode());
}
}
Multiple @U annotations merge URLs and ignore patterns. Scenario mode enables sequential responses for the same endpoint.
Record once. Test offline forever.
Join developers who've made the switch. Free forever. No credit card required.
🌾 MIT License | 🚀 Open Source | 💯 Free Forever