From 6cf9831cf603974836f98635dfa9ad0b786c0810 Mon Sep 17 00:00:00 2001 From: slayerjain Date: Sat, 22 Aug 2026 09:56:06 +0530 Subject: [PATCH 1/2] docs(running-keploy): add keploy mock guide and get-started quickstart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document `keploy mock record|replay` — using Keploy as a language-agnostic mocking framework for a user's own test suite. - mock-your-tests.md: the reference. Covers --on-miss fail|passthrough|record, --strict, the per-test scope API (KEPLOY_MOCK_AGENT + /agent/scope/begin|end) with pytest/go/jest examples, platform support, and a CI refresh recipe. - mock-quickstart.md: a hands-on "Get Started in 2 Minutes" walkthrough with a sample billing app whose tests call an external users API. Devs copy three files and follow record -> offline replay -> --on-miss record refresh, each step showing the actual captured keploy output. Both added to the version-4.0.0 running-keploy sidebar after custom-mocks. Files pass the repo's prettier check. Signed-off-by: slayerjain --- .../running-keploy/mock-quickstart.md | 239 ++++++++++++++++++ .../running-keploy/mock-your-tests.md | 165 ++++++++++++ .../version-4.0.0-sidebars.json | 3 + 3 files changed, 407 insertions(+) create mode 100644 versioned_docs/version-4.0.0/running-keploy/mock-quickstart.md create mode 100644 versioned_docs/version-4.0.0/running-keploy/mock-your-tests.md diff --git a/versioned_docs/version-4.0.0/running-keploy/mock-quickstart.md b/versioned_docs/version-4.0.0/running-keploy/mock-quickstart.md new file mode 100644 index 0000000000..a06ef1a3c5 --- /dev/null +++ b/versioned_docs/version-4.0.0/running-keploy/mock-quickstart.md @@ -0,0 +1,239 @@ +--- +id: mock-quickstart +title: "Get Started: Mock Your Tests in 2 Minutes" +sidebar_label: Mock Quickstart +description: A copy-paste walkthrough with a sample app — record the dependency calls its tests make, then replay them offline. Shows the real terminal output at each step. +tags: + - mocks + - quickstart + - pytest +keywords: + - keploy mock quickstart + - mock record + - mock replay + - getting started + - sample app +--- + +# Get Started: Mock Your Tests in 2 Minutes + +This is a hands-on walkthrough of [`keploy mock`](./mock-your-tests.md) with a +small sample app. You'll test an app that calls an external API, record the +API's responses, then run the same tests with the API **switched off**. Every +command shows the **real output** you should see — nothing is faked. + +## Prerequisites + +- Keploy installed — see [Installation](../server/installation.md): + + ```bash + keploy --version + ``` + +- **Linux** (root, for eBPF) or **Windows amd64** (Administrator). On **macOS**, + run your tests through a docker command (shown at the end). +- Python 3. (`go test` / `npm test` work identically — only the test command + changes.) + +## Step 1 — the sample app + +A tiny **billing app** that depends on an external **users API**. Three files in +one folder: + +`users_api.py` — the external dependency (stands in for a real upstream service): + +```python +import http.server, json, socketserver, sys +PORT = int(sys.argv[1]) +users = {"1": {"id": 1, "name": "Ada Lovelace", "plan": "pro"}, + "2": {"id": 2, "name": "Alan Turing", "plan": "free"}} +plans = {"free": {"price": 0}, "pro": {"price": 20}} + +class H(http.server.BaseHTTPRequestHandler): + def do_GET(self): + if self.path.startswith("/plans/"): + p = plans.get(self.path.rsplit("/", 1)[-1]); body = json.dumps(p or {}).encode() + else: + u = users.get(self.path.rsplit("/", 1)[-1]); body = json.dumps(u or {"error": "not found"}).encode() + self.send_response(200); self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))); self.end_headers(); self.wfile.write(body) + def log_message(self, *a): pass + +socketserver.TCPServer.allow_reuse_address = True +with socketserver.TCPServer(("127.0.0.1", PORT), H) as s: + s.serve_forever() +``` + +`billing.py` — **your app**: business logic that calls the users API: + +```python +"""A tiny billing app that depends on an external users API.""" +import json, urllib.request +USERS_API = "http://127.0.0.1:8085" + +def _get(path): + with urllib.request.urlopen(f"{USERS_API}{path}", timeout=5) as r: + return json.load(r) + +def monthly_bill(user_id: int) -> str: + """Look up a user and format their monthly bill.""" + user = _get(f"/users/{user_id}") + badge = "PRO" if user["plan"] == "pro" else "FREE" + price = 20 if user["plan"] == "pro" else 0 + return f'{user["name"]} ({badge}): ${price}/mo' +``` + +`test_billing.py` — **your tests**, which exercise the app: + +```python +from billing import monthly_bill + +def test_pro_user_bill(): + assert monthly_bill(1) == "Ada Lovelace (PRO): $20/mo" + +def test_free_user_bill(): + assert monthly_bill(2) == "Alan Turing (FREE): $0/mo" +``` + +Start the dependency in one terminal: + +```bash +python3 users_api.py 8085 +``` + +Your tests pass against the live API — that's the state you want to capture. + +## Step 2 — record + +In another terminal, run your test command under `keploy mock record`: + +```bash +keploy mock record -c "python3 -m pytest -q" +``` + +``` +🐰 Keploy: INFO Recording mocks for your test command {"mock-set": "billing", "command": "python3 -m pytest -q"} +🐰 Keploy: INFO Successfully cleared old mocks for refresh. {"testSet": "billing"} +🐰 Keploy: INFO Starting Application : {"executing_cmd": "/usr/bin/sh -c python3 -m pytest -q"} +2 passed in 0.01s +🐰 Keploy: INFO recorded mocks {"mocks": 2, "mock-set": "billing"} +🐰 Keploy: INFO test command finished successfully {"phase": "record"} +``` + +Keploy captured the two `/users/…` calls `billing.py` made into +`keploy/billing/mocks.yaml`: + +```yaml +# Generated by Keploy +version: api.keploy.io/v1beta1 +kind: Http +name: mock-0 +spec: + metadata: + type: HTTP_CLIENT + req: + method: GET + url: /users/1 + header: + Host: 127.0.0.1:8085 + resp: + status_code: 200 + body: '{"id": 1, "name": "Ada Lovelace", "plan": "pro"}' +``` + +Commit `keploy/` to your repo like a VCR cassette. Re-recording the same set +**overwrites it in place**, so refreshing on a merge to `main` is a clean diff. + +## Step 3 — replay with the dependency off + +**Stop `users_api.py`** (Ctrl+C in the first terminal). Then run the same tests +under `keploy mock replay`: + +```bash +keploy mock replay -c "python3 -m pytest -q" +``` + +``` +🐰 Keploy: INFO Replaying mocks for your test command {"mock-set": "billing", "command": "python3 -m pytest -q", "on-miss": "fail"} +🐰 Keploy: INFO Starting Application : {"executing_cmd": "/usr/bin/sh -c python3 -m pytest -q"} +2 passed in 0.01s +🐰 Keploy: INFO mock replay summary {"loaded": 2, "consumed": 2, "missed": 0} +🐰 Keploy: INFO test command finished successfully {"phase": "replay"} +``` + +Your app's tests pass with **no users API running** — Keploy served every call +from the recorded mocks. That's the whole point: fast, hermetic tests with no +live dependency. + +## Step 4 — a new dependency call (refresh) + +You add a feature that calls a **new endpoint**. Extend the app and its test: + +```python +# billing.py +def plan_price(plan_id: str) -> int: + return _get(f"/plans/{plan_id}")["price"] # a new endpoint + +# test_billing.py +def test_pro_plan_price(): + from billing import plan_price + assert plan_price("pro") == 20 +``` + +Start the dependency again and replay with `--on-miss record`. Known calls are +served from mocks; the new `/plans/pro` call goes to the real API **and is +appended** to the set: + +```bash +keploy mock replay -c "python3 -m pytest -q" --on-miss record +``` + +``` +🐰 Keploy: INFO on-miss: served an unrecorded call from the real dependency {"method": "GET", "url": "/plans/pro", "policy": "record"} +3 passed in 0.01s +🐰 Keploy: INFO appended new dependency calls to the mock set (--on-miss record) {"new": 1, "mock-set": "billing"} +``` + +Stop the dependency and replay normally — all three tests now pass from mocks: + +``` +3 passed in 0.01s +🐰 Keploy: INFO mock replay summary {"loaded": 3, "consumed": 3, "missed": 0} +``` + +:::note New endpoint vs new path parameter +`--on-miss record` fires only on a call that matches **no** recorded mock. +Keploy's HTTP matcher treats dynamic-looking path segments (numeric IDs, UUIDs) +as wildcards, so `monthly_bill(3)` → `/users/3` would match the recorded +`/users/1` mock and is **not** a miss — only a new endpoint like `/plans/pro` +is. Pass `--disableAutoURLDynamic` for strict URL matching, or re-record. +::: + +## Step 5 — it fails the build when your tests fail + +Keploy mirrors the runner's exit code, so it gates CI. If a test fails during +replay: + +``` +1 failed, 2 passed in 0.01s +🐰 Keploy: ERROR error while running the app {"error": "exit status 1"} +🐰 Keploy: INFO test command exited non-zero; mirroring its exit code {"phase": "replay", "exitCode": 1} +``` + +```bash +echo $? # -> 1 +``` + +## macOS + +Run your tests through a container and point Keploy at that command: + +```bash +keploy mock record -c "docker compose run --rm tests" +keploy mock replay -c "docker compose run --rm tests" +``` + +## Next + +- Full reference, `--strict`, and the per-test **scope API** (isolate mocks per + test from pytest/go/jest): [Mock Your Own Tests](./mock-your-tests.md). diff --git a/versioned_docs/version-4.0.0/running-keploy/mock-your-tests.md b/versioned_docs/version-4.0.0/running-keploy/mock-your-tests.md new file mode 100644 index 0000000000..a1823422bb --- /dev/null +++ b/versioned_docs/version-4.0.0/running-keploy/mock-your-tests.md @@ -0,0 +1,165 @@ +--- +id: mock-your-tests +title: Mock Your Own Tests (pytest, go test, jest) +sidebar_label: Mock Your Tests +description: Use Keploy as a language-agnostic mocking framework for your existing test suite — record the real dependency calls your tests make, then replay them so the suite runs without the real dependencies. +tags: + - mocks + - mocking + - pytest + - go test + - jest +keywords: + - keploy mock + - mock record + - mock replay + - vcr + - dependency mocking +--- + +# 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 ""` runs your tests and captures the + real outgoing dependency calls (HTTP, MySQL, …) into a named **mock set**. +- `keploy mock replay -c ""` 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 + +```bash +# 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. + +```bash +# 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. | + +```bash +# 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": ""} +POST {KEPLOY_MOCK_AGENT}/agent/scope/end {"name": ""} +``` + +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. + +:::note Sequential execution +Per-test scoping narrows a **single** served mock pool per test, so it assumes +your tests run **sequentially**. With parallel workers (pytest-xdist, +`go test`-parallel, jest workers) the scopes would overlap and stomp each other — +run those suites **suite-level** (omit the scope calls, or record without them): +suite-level replay serves the whole set to every test and is safe under +parallelism. +::: + +**pytest** (`conftest.py`): + +```python +import os, json, urllib.request, pytest + +AGENT = os.environ.get("KEPLOY_MOCK_AGENT") + +def _post(path, body): + if not AGENT: + return + 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", {"name": request.node.name}) + yield + _post("/agent/scope/end", {"name": request.node.name}) +``` + +**go test** (`TestMain` helper): + +```go +func scope(path, name string) { + agent := os.Getenv("KEPLOY_MOCK_AGENT") + if agent == "" { + return + } + body, _ := json.Marshal(map[string]string{"name": name}) + 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) follows the same +two calls with the test's name. + +## 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: + +```bash +# 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). diff --git a/versioned_sidebars/version-4.0.0-sidebars.json b/versioned_sidebars/version-4.0.0-sidebars.json index c1df70d9aa..c33c2e3b41 100644 --- a/versioned_sidebars/version-4.0.0-sidebars.json +++ b/versioned_sidebars/version-4.0.0-sidebars.json @@ -48,6 +48,9 @@ "running-keploy/configuration-file", "running-keploy/recording-filters", "running-keploy/custom-mocks", + "running-keploy/mock-quickstart", + + "running-keploy/mock-your-tests", "running-keploy/keploy-templatize", "running-keploy/risk-profile-analysis", "keploy-cloud/time-freezing", From bf0160bb5fe14b1a395ac30b8c585a66bee06d74 Mon Sep 17 00:00:00 2001 From: slayerjain Date: Sat, 22 Aug 2026 16:43:55 +0530 Subject: [PATCH 2/2] docs(mock): document per-PID scoping for parallel test workers The scope API now takes an optional `pid`; passing the worker's PID scopes the served mock pool per worker so Playwright/jest workers, pytest-xdist and `go test` -parallel each get only their own test's mocks. Replaces the "sequential only" caveat with the parallel guidance and adds the pid to the pytest / go / playwright snippets. Omitting pid keeps the sequential fallback. Signed-off-by: slayerjain --- .../running-keploy/mock-your-tests.md | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/versioned_docs/version-4.0.0/running-keploy/mock-your-tests.md b/versioned_docs/version-4.0.0/running-keploy/mock-your-tests.md index a1823422bb..3e0e862ed7 100644 --- a/versioned_docs/version-4.0.0/running-keploy/mock-your-tests.md +++ b/versioned_docs/version-4.0.0/running-keploy/mock-your-tests.md @@ -81,21 +81,27 @@ through a tiny HTTP API. Keploy exports the agent's address into your test proce as **`KEPLOY_MOCK_AGENT`**; call it at the start and end of each test: ``` -POST {KEPLOY_MOCK_AGENT}/agent/scope/begin {"name": ""} -POST {KEPLOY_MOCK_AGENT}/agent/scope/end {"name": ""} +POST {KEPLOY_MOCK_AGENT}/agent/scope/begin {"name": "", "pid": } +POST {KEPLOY_MOCK_AGENT}/agent/scope/end {"name": "", "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. -:::note Sequential execution -Per-test scoping narrows a **single** served mock pool per test, so it assumes -your tests run **sequentially**. With parallel workers (pytest-xdist, -`go test`-parallel, jest workers) the scopes would overlap and stomp each other — -run those suites **suite-level** (omit the scope calls, or record without them): -suite-level replay serves the whole set to every test and is safe under -parallelism. +:::tip Parallel workers +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 `; containerized workers in a separate namespace should run +suite-level. ::: **pytest** (`conftest.py`): @@ -105,9 +111,10 @@ import os, json, urllib.request, pytest AGENT = os.environ.get("KEPLOY_MOCK_AGENT") -def _post(path, body): +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: @@ -117,9 +124,9 @@ def _post(path, body): @pytest.fixture(autouse=True) def keploy_scope(request): - _post("/agent/scope/begin", {"name": request.node.name}) + _post("/agent/scope/begin", request.node.name) yield - _post("/agent/scope/end", {"name": request.node.name}) + _post("/agent/scope/end", request.node.name) ``` **go test** (`TestMain` helper): @@ -130,15 +137,30 @@ func scope(path, name string) { if agent == "" { return } - body, _ := json.Marshal(map[string]string{"name": name}) + // 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) follows the same -two calls with the test's 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: + +```js +// 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