Skip to main content
Back to Blog
API Testing
2026-09-28

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 conceptWhat it controlsQA use
ModeWhether Hoverfly captures, simulates, spies, synthesizes, modifies, or diffs trafficPick the runtime behavior for a test stage
Simulation JSONStored request and response pairs plus metadataVersion dependency behavior with the test suite
Request matcherHow an incoming request is compared with stored trafficMake tests stable without accepting wrong calls
MiddlewareExternal executable or remote logic that changes traffic or generates responsesSimulate faults, dynamic APIs, and latency
JournalRequests and responses observed by HoverflyVerify 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.

ModeBehaviorUse it when
CaptureProxies requests to the real service and records responsesYou need a first simulation from known-good behavior
SimulateServes stored responses and does not call the real serviceCI and deterministic integration tests
SpySimulates on match, calls the real API on missGradual migration from live dependency to simulation
SynthesizeUses middleware to generate responses instead of stored pairsThe API is too dynamic to capture cleanly
ModifySends live traffic through middleware without saving itYou need to alter requests or responses during exploratory tests
DiffCalls the real service and compares the real response with stored simulationYou want drift detection against a dependency
WebserverServes simulations as a webserver instead of acting as a proxyYour 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:

MatcherGood useRisk
exactHTTP method, stable path, fixed content typeToo brittle for timestamps and ids
globSimple path or host wildcardsCan become too broad
regexAnchored ids, versioned routes, constrained formatsHard to read if overused
jsonFull request body equalityFails when harmless fields reorder or expand
jsonPartialMatch the business fields that matterCan miss unwanted extra fields
jsonpathAssert a value exists inside a larger bodyNeeds meaningful follow-up matcher
jwt and jwtjsonpathMatch claims without comparing whole tokensDoes 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 shapePrefer proxy modePrefer webserver mode
Capturing real trafficYesNo
Client cannot change base URLsYesNo
App supports dependency base URL configMaybeYes
HTTPS certificate behavior mattersYesMaybe
CI service container with simple routesMaybeYes
Multiple dependencies with the same pathsYesMaybe, 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:

SymptomLikely causeFix
No journal entryApp did not call HoverflyCheck base URL, proxy settings, container networking
Journal entry exists but no response matchMatcher too strict or wrong addressing modeInspect path, destination, scheme, headers, and body matchers
Test passes locally but not in CIHostnames or ports differPrefer webserver mode with explicit base URL in CI
Capture works but webserver replay failsSimulation includes destination assumptionsRemember webserver requests use Hoverfly host and path
Diff reports timestamp and trace changes onlyVolatile fields in dependency responseIgnore 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.

NeedHoverfly fitNotes
Capture real HTTP behaviorStrongStart with capture, then curate
Schema-first contract enforcementLimitedPair with schema tools if contract validation is the main goal
Java JUnit service virtualizationStrongHoverfly Java has JUnit 5 extension support
Non-HTTP protocolsWeakHoverfly focuses on HTTP and HTTPS
Dynamic response generationStrongUse templating or middleware
Hermetic CIStrongUse simulate or webserver mode, not spy
Consumer contract governanceMediumSimulations 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.