When your service calls an external API, the test suite needs a stand-in for that API. Record-and-replay tools solve this by intercepting real HTTP traffic, saving the request/response pairs to disk, and replaying them on subsequent test runs. The idea is elegant and the tooling is mature. But every mainstream implementation shares a critical property worth understanding before you reach for one in a team environment: recordings are cleartext, written to disk, and almost always committed to version control.
This guide covers the four tools that dominate the space (Ruby VCR, Polly.js, nock, and WireMock) with honest notes on where each fits and where each breaks down. If you want a quick decision: for solo projects or isolated unit tests, any of these will serve you well. For team-shared fixtures generated from production-like traffic under a compliance regime, read through to the end.
Ruby VCR
VCR is the original. Released around 2010 for Ruby, it popularised the cassette metaphor: run your test once against the real endpoint and VCR records the exchange to a YAML or JSON file (the cassette); subsequent runs read from the file instead of hitting the network. The model is simple enough that nearly every record-and-replay tool since has borrowed it.
VCR supports multiple HTTP adapter libraries (Net::HTTP, Faraday, HTTParty, and others) and provides a flexible cassette matching system. You can match requests by URI, method, body, headers, or a custom matcher. Re-recording is controlled: cassettes can be set to record: :none (never hit the network), record: :new_episodes (record only misses), or record: :all (re-record every run).
The sensitive data story: cassettes are written verbatim by default. VCR provides filter_sensitive_data, which does regex substitution on cassette content before writing and after reading. It works, but it is opt-in and per-field. You must enumerate every sensitive key path or value pattern yourself. Miss one field, whether an internal correlation ID, a vendor-specific token, or a Set-Cookie header, and that value ends up in your repository. There is no fail-closed mode; the default behavior is to record everything.
VCR is the right choice if you’re in a Ruby shop, you want a battle-tested library with a large ecosystem, and you’re willing to maintain your own filter_sensitive_data rules. For a solo project or a narrow integration test, the overhead is low. For a growing API surface with multiple contributors, the risk is the same as it is everywhere: filters are as complete as whoever last updated them.
Polly.js
Polly.js is Netflix’s JavaScript record-and-replay library, released in 2018. It integrates with Node and browser environments via adapters for XHR, fetch, and Node’s http/https modules. Recordings are stored through a persister: the filesystem persister writes JSON files, and custom persisters can write to a REST API or in-memory storage.
The API is declarative and readable. You configure Polly in a test setup block, set a recording mode (record, replay, passthrough, or offline), and Polly handles the rest. Mode transitions are explicit, which makes it easy to move from recording to replay without test-by-test changes.
Like VCR, Polly has request filtering hooks. filteringRequestBody lets you scrub request bodies before recording; you can modify stored responses as they’re replayed. The primitives exist; none of it is automatic. An unannotated field is recorded as-is.
Polly.js is well-suited for JavaScript teams that want programmatic control over recording behavior and want to store cassettes in a format they can inspect and edit. The persister abstraction is a genuine engineering advantage: it’s straightforward to write a persister that encrypts before writing to disk, though no such persister ships out of the box.
nock
nock is a Node.js HTTP interception library, and it operates differently from VCR or Polly. Its primary mode is declaring expected requests and stubbed responses in test code:
nock('https://api.example.com')
.get('/users/1')
.reply(200, { id: 1, name: 'Test User' });
This is not recording; it’s manual stubbing. But nock also has a recording mode, nock.back, that works more like VCR: enable it in a test, and nock will intercept real HTTP requests, record the exchanges to a fixture file, and replay from that file on subsequent runs.
In nock.back mode, recordings are JSON files written to a directory you specify. The recorded format is human-readable and editable, which is useful for adding intentional edge cases. There is no built-in sensitive data filtering. Whatever your HTTP call returns, nock records it.
The fit: nock is the right tool when you want lightweight, in-code HTTP stubs for unit tests and don’t need the full overhead of a record-and-replay system. For narrow, controlled integration tests where you know exactly what the responses look like, manual nock declarations are hard to beat for clarity and speed. nock.back recording is convenient for getting initial fixtures from a real endpoint, but production-traffic recording is not the intended use case.
WireMock
WireMock is a Java-based mock server that runs as a standalone process or embedded in a JVM test suite. Unlike the other tools here, which intercept outbound HTTP at the library level, WireMock operates as a real HTTP server: your application routes requests through it, and WireMock stubs or proxies them. This makes it language-agnostic in practice: a Node, Python, or Go service can use WireMock as its mock server as long as it can point at a configurable base URL.
WireMock’s record-and-playback proxy is its most powerful feature for this use case. You configure WireMock to proxy to a real upstream and enable recording; it writes the request/response pairs to stub mapping files in JSON. You then replay those stubs in CI. The stub mappings include response bodies as literal JSON or in separate body files.
WireMock’s stateful stubs and scenario system are a genuine differentiator: you can model multi-step interactions where response B depends on whether request A was previously received. This level of fidelity is difficult to achieve in file-based cassette systems and matters for flows like OAuth token exchange, paginated retrieval, or anything that depends on server-side state.
The sensitive data situation is the same as everywhere else: recordings are plaintext. WireMock has request/response transformers and a removeHeaders option, but field-level filtering requires a custom transformer. Again: opt-in, manual, fail-open.
WireMock is the right choice for JVM shops that already have it in the stack, for polyglot environments that want a language-agnostic mock server, and anywhere stateful interaction modeling is genuinely needed.
Other ecosystems
go-vcr (Go) and vcrpy (Python) apply the same cassette pattern to their respective ecosystems. Both write YAML or JSON cassettes verbatim by default; both provide filter hooks that are opt-in. If you’re in one of those ecosystems and want the cassette model, reach for the native library rather than shoehorning WireMock. The trade-offs are the same.
Comparison
| Tool | Ecosystem | Recording model | Storage format | Sensitive data filtering | Stateful stubs |
|---|---|---|---|---|---|
| Ruby VCR | Ruby | Outbound HTTP intercept | YAML / JSON cassettes | filter_sensitive_data (opt-in, per-field) |
No |
| Polly.js | JavaScript | Outbound HTTP intercept | JSON via persister | Request/response hooks (opt-in) | No |
| nock | Node.js | Outbound HTTP intercept (nock.back) |
JSON fixture files | None built-in | No |
| WireMock | JVM / any | Proxy server with record mode | JSON stub mappings | Custom transformers (opt-in) | Yes (scenarios) |
| go-vcr | Go | Outbound HTTP intercept | YAML / JSON cassettes | Filter hooks (opt-in) | No |
| vcrpy | Python | Outbound HTTP intercept | YAML / JSON cassettes | Filter hooks (opt-in) | No |
The sensitive data gap
All of the tools above default to recording cleartext. Their filtering primitives are available, opt-in, and manual. You enumerate the fields you want scrubbed; anything you don’t enumerate is recorded as-is. This means:
- A new response field added by the upstream API goes unfiltered until someone updates the filter rules.
- There is no mechanism to detect that a field contains PII if it wasn’t tagged at configuration time.
- The cassette file in your repository reflects whatever the filter rules covered at the moment of recording, not necessarily everything the response contained.
This is a structural property of the file-based cassette model, not a bug in any particular library. The libraries cannot know which fields are sensitive without being told. And the default, which is to record when in doubt, is fail-open.
For a solo developer running tests against a staging environment, this is usually acceptable. For a team committing fixture files generated from production traffic, it’s a real risk. The compliance framing: GDPR requires a lawful basis for processing personal data. Fixture files derived from production responses, committed to a version-controlled repository accessible to all contributors, is processing without a clear basis.
When each OSS tool is the right choice
Solo project or prototype: Any of these will do the job. Pick the one that matches your language and keep the filter list current.
Unit-level HTTP isolation: nock in manual mode, or VCR with narrow, well-understood response shapes. Cassettes are small and easy to inspect.
JVM shop with stateful interactions: WireMock. The scenario system is worth the overhead if you need it.
Polyglot environment with an existing WireMock investment: Stay with WireMock. The standalone server model makes cross-language use straightforward.
Where a mask-at-edge approach fits instead
When fixtures need to come from real production traffic, because hand-written or hand-filtered fixtures don’t capture the edge cases that production actually surfaces, the cleartext cassette model creates a problem that filter hooks alone don’t fully solve.
The alternative is to move masking earlier in the pipeline: before anything leaves your infrastructure, before any file is written, before any network call is made. Masking happens inside your own service’s process. The only thing that crosses a network boundary is a payload that’s already been scrubbed. The cassette is never written to disk in cleartext.
This model also changes how fixture management works at the team level. Rather than each developer maintaining their own cassette directory, a shared, versioned fixture store accumulates traffic shapes from a realistic environment. Fingerprinting deduplicates structurally identical shapes, so the store grows with the variety of real traffic rather than with its volume.
Stubsmith implements this approach: an Express middleware captures traffic, masking happens inside your infrastructure before transmission, and generated stubs are shared across the team. See pricing for plan details; a free tier is available.
The right tool depends on the context. If you control the API you’re stubbing, the surface is narrow, and you’re confident in your filter rules, an OSS cassette library is a reasonable choice with no external dependency. If you’re integrating with third-party APIs you don’t control, operating in a regulated environment, or want fixtures that reflect real production behavior without moving production data, the investment in edge-side masking earns itself.