Hoverfly: API Simulation and Service Virtualization for Testing
A practical hoverfly guide for API simulation, capture, request matching, templating, Java tests, and CI workflows that reduce flaky dependencies.
Hoverfly: API Simulation and Service Virtualization for Testing
Hoverfly is an open source API simulation and service virtualization tool for replacing HTTP and HTTPS dependencies during development and testing. It can capture real traffic, export it as simulation JSON, replay that traffic, pass through on misses, synthesize responses through middleware, modify live traffic, and compare stored simulations against real responses. The project is still active and the GitHub repository says Hoverfly is developed and maintained by iOCO Solutions.
Version status: Hoverfly v1.12.15 (September 21, 2026) is the latest release, and the official documentation is built from it. Pin that version for binaries and the Docker image, and check your install channel (Homebrew, Docker, or release archive) before upgrading, because each one updates on its own schedule.
For QA engineers, Hoverfly is strongest when the hard problem is unstable dependencies rather than formal contract governance. Use it when you need realistic HTTP behavior, captured edge cases, dynamic templated responses, latency or failure injection, and repeatable simulations that can live in your test repo. If you need broad contract cataloging across OpenAPI, AsyncAPI, and gRPC, compare it with heavier platforms. If you need Java-friendly HTTP virtualization with a mature ecosystem, also compare it with WireMock. For strategy and tool selection, the broader API mocking and service virtualization guide is the companion read.
The Mental Model: Hoverfly Is Traffic-Centric
Hoverfly is not primarily a schema validator. It is a traffic virtualization runtime. It records or accepts request and response pairs, then uses request matchers to decide which stored response to serve. That model is powerful because your simulations can reflect real dependency behavior, including headers, path structures, dynamic response bodies, stateful sequences, and slow or failing endpoints.
The official docs describe the simulation file as JSON with request matchers, responses, delays, and metadata. Captured simulations can be edited by hand, generated by code, imported into Hoverfly, and shared between developers or CI jobs. The matching logic defaults to exact matching on captured request fields, but can be loosened with matchers such as glob, regex, JSON, JSON partial, JSONPath, form, array, JWT, JWT JSONPath, and matcher chaining.
| Hoverfly concept | What it controls | QA use |
|---|---|---|
| Mode | Whether Hoverfly captures, simulates, spies, synthesizes, modifies, or diffs traffic | Pick the runtime behavior for a test stage |
| Simulation JSON | Stored request and response pairs plus metadata | Version dependency behavior with the test suite |
| Request matcher | How an incoming request is compared with stored traffic | Make tests stable without accepting wrong calls |
| Middleware | External executable or remote logic that changes traffic or generates responses | Simulate faults, dynamic APIs, and latency |
| Journal | Requests and responses observed by Hoverfly | Verify side effects and debug missing matches |
What people get wrong is recording a simulation and treating it as permanent truth. Captured traffic is a starting point. You still need to remove secrets, loosen volatile matchers, preserve meaningful matchers, and review the response bodies. A good simulation is curated. A raw capture often contains timestamps, authorization headers, request ids, and environment-specific hosts that will break the next test run.
Modes That Matter in Real Test Suites
The official Hoverfly command reference lists flags for modes such as -capture, -diff, -modify, -spy, -synthesize, and -webserver. The hoverctl mode command is the usual way to switch a running local instance. Each mode answers a different testing question, so choose deliberately.
| Mode | Behavior | Use it when |
|---|---|---|
| Capture | Proxies requests to the real service and records responses | You need a first simulation from known-good behavior |
| Simulate | Serves stored responses and does not call the real service | CI and deterministic integration tests |
| Spy | Simulates on match, calls the real API on miss | Gradual migration from live dependency to simulation |
| Synthesize | Uses middleware to generate responses instead of stored pairs | The API is too dynamic to capture cleanly |
| Modify | Sends live traffic through middleware without saving it | You need to alter requests or responses during exploratory tests |
| Diff | Calls the real service and compares the real response with stored simulation | You want drift detection against a dependency |
| Webserver | Serves simulations as a webserver instead of acting as a proxy | Your app can point directly at a fake base URL |
The most important operational difference is proxy versus webserver. In proxy mode, clients call the real URL but route through Hoverfly as an HTTP proxy. That is what makes capture work. In webserver mode, your application points directly at Hoverfly as the base URL. The docs state that webserver mode cannot capture traffic, and can only be used to simulate and synthesize APIs. That limitation is a frequent source of confused test failures.
Capture Once, Curate Before Commit
A practical Hoverfly workflow starts with capture mode, but it should not end there. Capture against a stable fixture, export the simulation, edit matchers and sensitive values, then import the curated file in simulate mode. The official creating and exporting tutorial shows hoverctl start, hoverctl mode capture, a proxied curl, hoverctl export, and hoverctl stop. It also notes that request headers are not captured by default unless you request specific headers or all headers.
hoverctl start
hoverctl mode capture --headers 'Content-Type,Authorization'
curl --proxy http://localhost:8500 \
http://orders.internal.test/v1/orders/1001
hoverctl export test/simulations/orders.raw.json
hoverctl stop
Before committing orders.raw.json, inspect the matchers. You probably do not want to match the entire Authorization header exactly. You probably do want to match method, path, and the part of the body that expresses business intent. The point is not to make everything loose. The point is to be strict where product behavior matters and flexible where infrastructure noise changes.
The docs also show hoverctl export --url-pattern, which is useful when one capture session includes several dependencies. Export each dependency to a focused file so a test failure tells you which virtual service was involved.
hoverctl start
hoverctl mode capture --all-headers
curl --proxy http://localhost:8500 http://orders.internal.test/v1/orders/1001
curl --proxy http://localhost:8500 http://payments.internal.test/v1/payments/1001
hoverctl export orders.json --url-pattern 'orders.internal.test'
hoverctl export payments.json --url-pattern 'payments.internal.test'
hoverctl stop
That workflow also makes AI coding agents safer. Ask the agent to edit orders.json only when changing order behavior. Do not let it rewrite every simulation in a directory because one assertion failed.
Simulation JSON You Can Review
Hoverfly simulations are JSON, which makes them reviewable in pull requests. The schema version used in common examples is v5.2. The request contains arrays of matchers for fields such as path, method, destination, scheme, query, headers, and body. The response contains status, body or bodyFile, headers, optional delay, and whether the body is templated.
{
"data": {
"pairs": [
{
"request": {
"method": [
{
"matcher": "exact",
"value": "GET"
}
],
"path": [
{
"matcher": "regex",
"value": "^/v1/orders/[0-9]+$"
}
],
"destination": [
{
"matcher": "exact",
"value": "orders.internal.test"
}
],
"scheme": [
{
"matcher": "exact",
"value": "http"
}
]
},
"response": {
"status": 200,
"body": "{\"orderId\":\"{{ Request.Path.[2] }}\",\"status\":\"accepted\"}",
"encodedBody": false,
"templated": true,
"headers": {
"Content-Type": [
"application/json"
]
}
}
}
]
},
"meta": {
"schemaVersion": "v5.2"
}
}
This example uses a regex matcher for the path, but it is anchored. That matters. A loose expression that matches any string containing orders could hide a route bug. A precise expression still allows different order ids while rejecting malformed paths.
Request matchers are where most Hoverfly test quality lives:
| Matcher | Good use | Risk |
|---|---|---|
exact | HTTP method, stable path, fixed content type | Too brittle for timestamps and ids |
glob | Simple path or host wildcards | Can become too broad |
regex | Anchored ids, versioned routes, constrained formats | Hard to read if overused |
json | Full request body equality | Fails when harmless fields reorder or expand |
jsonPartial | Match the business fields that matter | Can miss unwanted extra fields |
jsonpath | Assert a value exists inside a larger body | Needs meaningful follow-up matcher |
jwt and jwtjsonpath | Match claims without comparing whole tokens | Does not verify signatures by itself |
The matcher decision is a QA design decision, not a syntax chore. If the dependency call is part of a payment authorization flow, match amount, currency, merchant id, and idempotency key. If the dependency call is a harmless location lookup, path and country may be enough.
Templating Dynamic Responses Without Losing Determinism
Hoverfly templating is disabled by default and enabled by setting the response templated field to true. The current docs show request-derived values such as {{ Request.QueryParam.myParam }}, {{ Request.Path.[1] }}, {{ Request.Method }}, {{ Request.Host }}, {{ Request.Body 'jsonpath' '$.id' }}, {{ Request.Header.X-Header-Id }}, state, JWT helpers, and random functions.
Templating is ideal when the response should echo a request field, but it can make tests flaky if you lean on random values without assertions that tolerate them. Prefer echoing deterministic request data or state data. Use random values only when the test is specifically exercising client tolerance for unpredictable server output.
{
"data": {
"pairs": [
{
"request": {
"method": [
{
"matcher": "exact",
"value": "POST"
}
],
"path": [
{
"matcher": "exact",
"value": "/v1/refunds"
}
],
"body": [
{
"matcher": "jsonpath",
"value": "$.paymentId"
}
]
},
"response": {
"status": 201,
"body": "{\"refundId\":\"rf_{{ Request.Body 'jsonpath' '$.paymentId' }}\",\"state\":\"queued\"}",
"encodedBody": false,
"templated": true,
"headers": {
"Content-Type": [
"application/json"
]
}
}
}
]
},
"meta": {
"schemaVersion": "v5.2"
}
}
That simulation proves a useful point: a single request matcher can cover many payments while still requiring the body to contain paymentId. A weaker simulation that matches only POST /v1/refunds could let the client omit the field and still pass.
Proxy Mode Versus Webserver Mode in Tests
Proxy mode is the most faithful because the client still thinks it is calling the real host. It is also the mode that can capture. The tradeoff is client configuration. Your HTTP client must respect the proxy, and HTTPS traffic may require trusting Hoverfly's certificate for man-in-the-middle behavior.
Webserver mode is simpler for test configuration. Your app points at http://localhost:8500 or a container alias, and Hoverfly serves paths from the simulation. The official docs explain that when running as a webserver, Hoverfly strips the domain from the endpoint URL. That means a captured request to http://echo.example.test/key/value becomes a webserver request like http://localhost:8500/key/value.
| Test shape | Prefer proxy mode | Prefer webserver mode |
|---|---|---|
| Capturing real traffic | Yes | No |
| Client cannot change base URLs | Yes | No |
| App supports dependency base URL config | Maybe | Yes |
| HTTPS certificate behavior matters | Yes | Maybe |
| CI service container with simple routes | Maybe | Yes |
| Multiple dependencies with the same paths | Yes | Maybe, if you split simulations |
For modern test suites, I usually prefer webserver mode for deterministic CI simulation and proxy mode for capture or for clients that do not expose dependency base URLs. The split is clean: capture like a proxy, curate JSON, replay as directly as possible.
Hoverfly Java and JUnit 5
Hoverfly Java is released separately from Hoverfly itself; hoverfly-java-junit5 0.20.2 is the latest artifact on Maven Central, while parts of the documentation still show 0.20.1. The JUnit 5 integration centers on HoverflyExtension, with annotations such as @HoverflySimulate, @HoverflyCapture, @HoverflyDiff, and @HoverflyValidate. The extension can inject a Hoverfly object into tests and resets state between tests to reduce interference.
<dependencies>
<dependency>
<groupId>io.specto</groupId>
<artifactId>hoverfly-java-junit5</artifactId>
<version>0.20.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.14.0</version>
<scope>test</scope>
</dependency>
</dependencies>
package test.orders;
import io.specto.hoverfly.junit5.HoverflyExtension;
import io.specto.hoverfly.junit5.api.HoverflySimulate;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@HoverflySimulate(
source = @HoverflySimulate.Source(
value = "orders.json",
type = HoverflySimulate.SourceType.CLASSPATH
)
)
@ExtendWith(HoverflyExtension.class)
class OrderClientTest {
private final HttpClient client = HttpClient.newHttpClient();
@Test
void returnsSimulatedOrder() throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://orders.internal.test/v1/orders/1001"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode());
assertTrue(response.body().contains("\"status\":\"accepted\""));
assertTrue(response.body().contains("\"orderId\":\"1001\""));
}
}
The assertions check response status and body content. That matters because a test that only asserts 200 can pass while the client silently receives the wrong payload. If you use the Java DSL instead of simulation JSON, keep the same discipline: match requests meaningfully and assert payload side effects.
CI: Run Simulations Like Test Fixtures
In CI, commit simulations under a test directory, start Hoverfly in simulate or webserver mode, wait for the admin endpoint, run your tests, then export logs and journals when something fails. The Docker image documented by Hoverfly contains Hoverfly but not hoverctl, so use Hoverfly flags or the REST API when you want a container-only setup.
name: service-virtualization
on:
pull_request:
jobs:
hoverfly:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
- run: npm ci
- name: Start Hoverfly in webserver mode
run: |
docker run --detach --name hoverfly \
--publish 8500:8500 \
--publish 8888:8888 \
--volume "${{ github.workspace }}/test/simulations:/simulations:ro" \
spectolabs/hoverfly:latest \
-webserver \
-import /simulations/orders.json
- name: Wait for Hoverfly
run: |
for attempt in 1 2 3 4 5 6 7 8 9 10; do
curl --fail http://localhost:8888/api/v2/hoverfly/mode && exit 0
sleep 1
done
docker logs hoverfly
exit 1
- name: Run tests against simulated dependency
run: npm test -- --runInBand
env:
ORDERS_BASE_URL: http://localhost:8500
- name: Export Hoverfly diagnostics
if: always()
run: |
mkdir -p hoverfly-diagnostics
docker logs hoverfly > hoverfly-diagnostics/hoverfly.log
curl --silent http://localhost:8888/api/v2/journal > hoverfly-diagnostics/journal.json
- uses: actions/upload-artifact@v7
if: always()
with:
name: hoverfly-diagnostics-${{ github.run_id }}
path: hoverfly-diagnostics
The diagnostic step is not decoration. It tells you whether your application actually called the simulated dependency, what request Hoverfly saw, and why a matcher might have missed. A missing journal entry usually means your app never reached Hoverfly. A journal entry with no matching simulation usually means your matchers are too strict, too loose in the wrong place, or pointed at the wrong path because proxy and webserver addressing got mixed.
Failure Mode: Capture Passes, Simulate Misses
A realistic failure looks like this: capture mode records GET /v1/orders/1001 with an Authorization header, a trace header, and an exact query string. The developer commits the file. CI starts Hoverfly in simulate mode. The client calls GET /v1/orders/1002 with a new trace header. Hoverfly has no match, and the test fails even though the behavior should be identical for both order ids.
The diagnosis is matcher brittleness. Captured simulations default toward exact matches. That is safe for a raw recording but too narrow for a reusable fixture. Change the path matcher to an anchored regex for numeric order ids, remove or loosen volatile trace headers, and keep strict matching on method and required business query parameters.
Use this triage table before editing production code:
| Symptom | Likely cause | Fix |
|---|---|---|
| No journal entry | App did not call Hoverfly | Check base URL, proxy settings, container networking |
| Journal entry exists but no response match | Matcher too strict or wrong addressing mode | Inspect path, destination, scheme, headers, and body matchers |
| Test passes locally but not in CI | Hostnames or ports differ | Prefer webserver mode with explicit base URL in CI |
| Capture works but webserver replay fails | Simulation includes destination assumptions | Remember webserver requests use Hoverfly host and path |
| Diff reports timestamp and trace changes only | Volatile fields in dependency response | Ignore or normalize those fields outside the critical assertion |
Do not fix a matcher miss by replacing everything with glob: *. That turns service virtualization into a false-positive machine. The better repair is a targeted relaxation: exact method, anchored route, partial JSON body, and no volatile infrastructure headers.
Diff, Spy, Synthesize, and Modify in a QA Workflow
Simulate mode is the CI workhorse, but the other modes are worth knowing.
Spy mode is useful during migration. If a request has a matching simulation, Hoverfly responds. If it does not, Hoverfly passes the request to the real API. This lets you virtualize high-value endpoints first without blocking the whole test suite. The risk is accidental live dependency calls in CI. Use spy mode in controlled environments, not as the default for hermetic tests.
Diff mode forwards requests to the real service and compares the real response with the stored simulation. The official docs state that differences are stored and can be retrieved from GET /api/v2/diff, and cleaned with DELETE /api/v2/diff. This is useful for dependency drift checks: did the third-party API response shape change since the simulation was captured?
hoverctl start
hoverctl import test/simulations/orders.json
hoverctl mode diff
curl --proxy http://localhost:8500 \
http://orders.internal.test/v1/orders/1001
curl --silent http://localhost:8888/api/v2/diff
hoverctl stop
Synthesize mode requires middleware and generates responses on the fly instead of using stored simulation pairs. Use it for APIs whose response depends on state or calculations that are awkward to enumerate. Modify mode also requires middleware, but it changes live requests and responses instead of storing them. That can help reproduce partner API failures without changing your application code.
Middleware is powerful, but it is also executable code in your test path. Keep middleware small, reviewed, and deterministic. If middleware starts becoming a second implementation of the third-party API, stop and decide whether you need a dedicated fake service.
Decision Guide for Hoverfly
Hoverfly is a good fit when your tests need to behave as though real HTTP dependencies exist, but you cannot afford the flakiness, rate limits, credentials, or data volatility of those dependencies. It is especially strong for regression fixtures made from real traffic and for teams that want to review simulations as files.
| Need | Hoverfly fit | Notes |
|---|---|---|
| Capture real HTTP behavior | Strong | Start with capture, then curate |
| Schema-first contract enforcement | Limited | Pair with schema tools if contract validation is the main goal |
| Java JUnit service virtualization | Strong | Hoverfly Java has JUnit 5 extension support |
| Non-HTTP protocols | Weak | Hoverfly focuses on HTTP and HTTPS |
| Dynamic response generation | Strong | Use templating or middleware |
| Hermetic CI | Strong | Use simulate or webserver mode, not spy |
| Consumer contract governance | Medium | Simulations help, but they are not a full contract broker |
For AI coding agents, Hoverfly works best with guardrails. Put simulations under version control. Require the agent to read the journal before changing matchers. Require exact assertions on side effects, not only status codes. Ask it to explain why a matcher is exact, glob, regex, or JSON partial. Those small rules keep the agent from making the simulation so loose that it no longer protects you.
Frequently Asked Questions
Is Hoverfly the same as a mock server?
Hoverfly can act as a mock server, especially in webserver mode, but its broader model is service virtualization. It can capture real traffic, replay simulations, spy on misses, synthesize responses with middleware, modify live traffic, and diff stored simulations against real responses. A simple mock server is usually hand-authored. Hoverfly often starts from captured behavior and then turns that behavior into a curated test fixture.
Should I use proxy mode or webserver mode?
Use proxy mode when you need to capture traffic or when your client cannot easily change dependency base URLs. Use webserver mode when your application can point directly at a simulated base URL, especially in CI. Webserver mode is simpler, but the docs state it cannot capture traffic. Many teams capture through proxy mode, edit the simulation, then replay in webserver mode for deterministic tests.
How strict should Hoverfly request matchers be?
Strict enough to catch real client mistakes, but not so strict that harmless infrastructure values break tests. Match method, route, required query parameters, and important body fields. Avoid exact matching on trace ids, timestamps, generated tokens, and environment-specific headers unless those fields are the behavior under test. Prefer anchored regexes and JSON partial matchers over broad wildcard matching.
Can Hoverfly replace contract testing?
Not completely. Hoverfly is excellent for HTTP simulation and dependency behavior, but it does not replace every schema or provider contract check. It proves that your client behaves correctly against the simulated traffic you provide. If you also need to prove that a provider implements an OpenAPI schema or that consumers and providers negotiate changes formally, pair Hoverfly with contract or schema validation tooling.