Mock Your Own Tests
keploy mock lets you use Keploy as a mocking framework for your existing test
suite — pytest, go test, jest/playwright, or any command that makes network
calls. It is a language-agnostic VCR / WireMock that works at the network layer,
so your test code needs no SDK and no changes.
keploy mock record -c "<your test command>"runs your tests and captures the real outgoing dependency calls (HTTP, MySQL, …) into a named mock set.keploy mock replay -c "<your test command>"runs your tests again with those calls served from the mock set, so the real dependencies can be offline.
Keploy propagates your test runner's exit code, so it drops straight into CI.
Quick start
# 1. Record the dependency calls your tests make (real dependencies must be up)
keploy mock record -c "pytest"
# 2. Replay — the dependencies can now be down; your tests run against the mocks
keploy mock replay -c "pytest"
The mocks are written to keploy/default/mocks.yaml. Commit them like a VCR
cassette. Re-recording overwrites the set in place, so a "re-record on merge
to main" job produces a clean, reviewable diff.
# go test
keploy mock record -c "go test ./..."
keploy mock replay -c "go test ./..."
# a named set (e.g. per service)
keploy mock record -c "npm test" --name orders
keploy mock replay -c "npm test" --name orders
On-miss policy
When an outgoing call matches no recorded mock, --on-miss decides what happens:
--on-miss | Behaviour |
|---|---|
fail (default) | The call gets an error (deterministic); the run fails. |
passthrough | The call goes to the real dependency; nothing is persisted. |
record | The call goes to the real dependency and is appended to the set (VCR "new episodes") — incremental refresh. |
# A new test hit a new endpoint? Capture just that call and keep it:
keploy mock replay -c "pytest" --on-miss record
Add --strict to fail the run if any recorded mock was missed (a dependency
contract drifted), even when the tests themselves passed.
Per-test scoping (optional)
By default a set is recorded and replayed suite-wide. For per-test isolation — so
each test gets exactly its own mocks — your test runner can mark test boundaries
through a tiny HTTP API. Keploy exports the agent's address into your test process
as KEPLOY_MOCK_AGENT; call it at the start and end of each test:
POST {KEPLOY_MOCK_AGENT}/agent/scope/begin {"name": "<test name>", "pid": <worker pid>}
POST {KEPLOY_MOCK_AGENT}/agent/scope/end {"name": "<test name>", "pid": <worker pid>}
At record time this writes a per-test mappings.yaml; at replay time it restricts
the served pool to that test's mocks. No scope calls ⇒ suite-level, which is still
correct.
Include your worker's PID as pid (e.g. Node process.pid, Python
os.getpid()) and Keploy scopes the served pool per worker, so parallel
runners — Playwright/jest workers, pytest-xdist, go test -parallel — each get
only their own test's mocks with no cross-worker interference. Keploy attributes
an outgoing call to a worker by its process (walking the process tree), so calls
from a child process the worker spawns are covered too.
pid is optional: omit it and scoping falls back to a single shared pool that
assumes tests run sequentially (the pre-parallel behavior). Parallel scoping
assumes the runner and the Keploy agent share a PID namespace — the normal case
for keploy mock <cmd>; containerized workers in a separate namespace should run
suite-level.
pytest (conftest.py):
import os, json, urllib.request, pytest
AGENT = os.environ.get("KEPLOY_MOCK_AGENT")
def _post(path, name):
if not AGENT:
return
body = {"name": name, "pid": os.getpid()} # pid → per-worker isolation under pytest-xdist
req = urllib.request.Request(AGENT + path, data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"}, method="POST")
try:
urllib.request.urlopen(req, timeout=3).read()
except Exception:
pass
@pytest.fixture(autouse=True)
def keploy_scope(request):
_post("/agent/scope/begin", request.node.name)
yield
_post("/agent/scope/end", request.node.name)
go test (TestMain helper):
func scope(path, name string) {
agent := os.Getenv("KEPLOY_MOCK_AGENT")
if agent == "" {
return
}
// pid → per-worker isolation when tests run in parallel
body, _ := json.Marshal(map[string]any{"name": name, "pid": os.Getpid()})
http.Post(agent+path, "application/json", bytes.NewReader(body))
}
// In each test: scope("/agent/scope/begin", t.Name()); defer scope("/agent/scope/end", t.Name())
jest / playwright (a beforeEach/afterEach or reporter hook) makes the same
two calls with the test's name and process.pid — the pid is what keeps
Playwright's or jest's parallel workers isolated from each other:
// Playwright: in a fixture or beforeEach/afterEach
const post = (path, name) =>
fetch(`${process.env.KEPLOY_MOCK_AGENT}${path}`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({name, pid: process.pid}),
}).catch(() => {});
test.beforeEach(({}, testInfo) => post("/agent/scope/begin", testInfo.title));
test.afterEach(({}, testInfo) => post("/agent/scope/end", testInfo.title));
Platforms
| Platform | How to run |
|---|---|
| Linux | Native — keploy mock record -c "pytest" (uses eBPF; needs root). |
| Windows (amd64) | Native — same command, from an Administrator shell. |
| macOS (arm64) | Run your tests through a container, e.g. -c "docker compose run tests". |
Refresh in CI
Because re-recording overwrites the set in place and the runner's exit code is
propagated, refreshing mocks on a merge to main is a normal CI step:
# bring up the real dependencies, then:
keploy mock record -c "pytest" --name default
keploy sanitize # scrub secrets before committing
git add keploy/ && git commit -m "chore: refresh mocks" || echo "no changes"
On Keploy Cloud / Enterprise, keploy mock is registry-first: the set is
uploaded after record and downloaded before replay automatically. Pass --local
to keep everything on disk (the open-source behaviour).