Talk: "WireMock vs Interface-Based Stubs: Rethinking Integration Testing" A practical Spring Boot demo
Integration tests often depend on external services. Most teams reach for WireMock by default — but is it always the right choice?
This demo shows the same 5 business scenarios tested with two different approaches, so you can compare them side-by-side.
OrderService
↓
CustomerGateway (interface)
↓
CustomerService (external HTTP dependency)
Business rules in OrderService:
- Customer must exist
- Customer must be active
- Order amount must not exceed customer's credit limit
- Full Spring context loaded
HttpCustomerGatewaymakes real HTTP calls- WireMock intercepts at the network level
- HTTP serialization, status codes, headers all exercised
wireMockServer.stubFor(get(urlEqualTo("/customers/CUST-001"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{ "id": "CUST-001", "active": true, "creditLimit": 1000.0 }
""")));- No Spring context — plain Java objects
StubCustomerGatewayinjected directly- No HTTP, no JSON, no network
var gateway = new StubCustomerGateway()
.withActiveCustomer("CUST-001", 1000.0);
var orderService = new OrderService(gateway);| # | Scenario | Expected Result |
|---|---|---|
| 1 | Active customer, sufficient credit | CREATED |
| 2 | Inactive customer | REJECTED |
| 3 | Customer not found (404) | REJECTED |
| 4 | Customer service unavailable (500) | FAILED |
| 5 | Amount exceeds credit limit | REJECTED |
Both test classes cover all 5 scenarios.
| Criterion | WireMock | Interface Stub |
|---|---|---|
| Tests HTTP behaviour | ✅ | ❌ |
| Catches serialization bugs | ✅ | ❌ |
| Validates HTTP contract | ✅ | ❌ |
| Speed | ✅ Fast | |
| Setup complexity | ✅ Low | |
| Readability | ✅ High | |
| Compile-time safety | ❌ | ✅ |
| Refactor-friendly | ❌ | ✅ |
| Risk of contract drift |
The CustomerGateway interface is the crucial boundary that enables both approaches:
public interface CustomerGateway {
Customer getCustomer(String customerId);
}- Production →
HttpCustomerGateway(real HTTP) - WireMock tests →
HttpCustomerGateway+ WireMock server - Stub tests →
StubCustomerGateway(fake implementation)
The lesson: Good production design (dependency inversion) naturally enables both testing strategies. You don't need to bend your code for tests — you need well-defined boundaries.
WireMock is invaluable for testing HTTP contracts and integration behaviour. Interface-based stubs are better for fast feedback and business logic. Use both — but for different purposes.
Or put another way:
"When do mocks become lies?" When your stub drifts from the real service behaviour — and you don't know it.
# Run all tests
mvn test
# Run only WireMock tests
mvn test -Dtest="*WireMock*"
# Run only Stub tests
mvn test -Dtest="*Stub*"src/
├── main/java/com/demo/
│ ├── gateway/
│ │ ├── CustomerGateway.java ← The key interface
│ │ └── HttpCustomerGateway.java ← Production HTTP implementation
│ ├── service/
│ │ └── OrderService.java ← Business logic (system under test)
│ ├── model/
│ │ ├── Customer.java
│ │ ├── Order.java
│ │ └── OrderRequest.java
│ └── exception/
│ ├── CustomerNotFoundException.java
│ └── CustomerServiceException.java
│
└── test/java/com/demo/
├── wiremock/
│ └── OrderServiceWireMockTest.java ← Approach 1
└── stub/
├── StubCustomerGateway.java ← Fake implementation
└── OrderServiceStubTest.java ← Approach 2
- Java 17
- Spring Boot 3.2
- WireMock 3.3
- JUnit 5
- AssertJ
- Lombok
- Maven