Running Keploy on a Go App
Recording real traffic from a Go service and replaying it as tests — on Windows, through WSL.
Why Keploy
Integration tests for a Go service usually mean keeping a database alive, writing mocks by hand, and updating those mocks every time a schema changes. It's a lot of upkeep for tests that mostly verify things you already know work.
Keploy takes a different approach: it records real traffic once and replays it forever.
When you start a recording, Keploy moves your app to another port and takes the original one for itself, so every request passes through it. At the same time, it attaches an eBPF probe at the kernel level to watch the syscalls your app makes to the database.
your curl → Keploy (port 8082) → app (port 35555) → Postgres
↑ ↑
records request eBPF records this
and response query and its result
So one recording run captures two things: the HTTP exchange becomes a test case, and the database exchange becomes a mock.
On the test run, the app starts again — but this time Keploy answers the database calls from those mocks. Postgres never gets touched. No live database, no hand-written mocks, no drift when the schema changes.
Because the probe sits at the kernel level rather than inside your code, you don't import a library or change a line of your app. That's also why Keploy needs sudo, and why it only runs on Linux — hence WSL on Windows.
Prerequisites
For Windows users, Keploy runs three ways: natively, through WSL, or in Docker. This tutorial uses WSL.
You'll need WSL2 with Ubuntu, Docker Desktop, and Go.
Note
Version numbers won't line up. The docs site is on 3.0.0, its screenshots show 3.4.1, and the install script currently gives 3.8.2. Run keploy --version and trust that over anything written down — several commands below differ between versions.
This tutorial was written against 3.8.2.
Step 1 — Install Keploy
If WSL isn't enabled yet, run this in PowerShell as Administrator:
wsl --install -d Ubuntu-22.04Then open your WSL terminal and move to your Linux home directory:
cd ~Install the CLI:
curl --silent -O -L https://keploy.io/install.sh && source install.shIt will ask for your password. Nothing appears as you type — that's normal.
Verify:
keploy --versionIf yours isn't 3.8.2, some flags below may have changed.
Step 2 — Log in
sudo keploy loginGotcha
Do this before recording anything. The quickstart pages go straight from install to keploy record, and without a login that fails with a 401 that never mentions authentication.
Without sudo, it fails on a log file it can't write — and the message gives no hint that sudo is the fix:
Failed to start the logger for the CLI failed to set the log file permission to 777: chmod keploy-logs.txt: operation not permitted
The browser won't open inside WSL. Keploy prints a URL instead:
Opening browser for authentication...
If the browser does not open automatically, please visit:
https://app.keploy.io/signin/integration/auth?method=code&code=...
Paste it into a Windows browser and it works — but you have sixty seconds. Miss that window and you get:
authentication timed out after 1 minute; last polling error:
unexpected status 401: {"error":"invalid or expired code"}
Watch out
If you don't already have a Keploy account, sixty seconds isn't enough to sign up. Create one at app.keploy.io first, then run keploy login.
The timeout message points you at manual login, which skips the browser entirely. Go to Account → API Keys in the dashboard, generate a token, and paste it when the CLI prompts you. Keploy's installation docs cover this flow in full.
One difference: they show the command as keploy login --manual-login, but on WSL it needs sudo for the same log-file reason as above.
sudo keploy login --manual-loginStep 3 — Get the sample app
git clone https://github.com/keploy/samples-go.git && cd samples-go/echo-sql
go mod downloadgo mod download fetches the libraries this project needs, as listed in go.mod.
Note
Clone into your Linux home (~), not under /mnt/c/ — that's your Windows C: drive as WSL sees it. WSL can reach your Windows drives, but through a translation layer that makes file operations noticeably slower.
Step 4 — Start Postgres
Keploy offers two routes here: run everything in Docker, or run the database in Docker and the app natively. This tutorial takes the second route — the app runs directly on WSL, and only Postgres is containerised.
Worth knowing up front, because the two paths differ in several places and mixing them produces confusing failures. The next step is the first of those differences.
docker-compose up -d postgresThe project ships a docker-compose.yml that defines both the app and the database. Naming postgres at the end starts just that one service, and -d runs it in the background so you get your terminal back.
Confirm it's running:
docker psThat lists every running container. You should see one for Postgres, with port 5432 mapped. If nothing appears, the database didn't start and the next steps will fail.
Step 5 — Point the app at localhost
This is the first place the two routes diverge.
The sample ships configured for Docker, where the app and database share a Docker network and the app finds Postgres by name. On the native route the app runs outside Docker, so that name means nothing to it — it has to reach Postgres through the published port on localhost.
Open main.go and find the Postgres host configuration:.
Note
Since Line numbers drift whenever a file changes, so search for the string instead of counting:
grep -n "postgresDb" main.goOr view the top of the file with line numbers:
cat -n main.go | head -50Edit it with nano main.go, then confirm the change:
git diff main.goNote
On the Docker route, skip this step entirely. The docs mention changing the host there too, but docker-compose.yml already sets container_name: postgresDb and a matching network alias, so the shipped value resolves correctly. Leave it as it is.
Step 6 — Build the binary
go build -coverNote
The sample's README uses go build -cover; the quickstart docs use go build -o echo-psql-url-shortener. Either works — -o names the output file explicitly, but Go already names the binary after the module in go.mod, so you get echo-psql-url-shortener either way. Keep -cover; it's what enables coverage reporting later.
This compiles main.go, handler.go, and every library they import into a single executable.
New to Go? What this step actually does
If you've come from JavaScript or Python, this step is new. Those languages are interpreted: you ship source, and Node or the Python runtime reads it each time. Go compiles ahead of time into one standalone file that runs on its own — no source, no Go installation needed on the target machine. It's a large part of why Go is popular for backend services and CLI tools; Docker, Kubernetes and Keploy itself are all written in it.
Why Keploy needs a binary rather than go run: Keploy starts your app as a child process and watches its syscalls through eBPF, so it needs one concrete process to attach to. go run main.go would compile to a temporary location and spawn a second process to run it — leaving Keploy watching the compiler's wrapper instead of your server.
The -cover flag compiles in coverage instrumentation, which lets Keploy report which lines your recorded tests actually exercised.
Step 7 — Record
sudo PATH="$PATH" keploy record -c "./echo-psql-url-shortener"Keploy starts your app, takes over port 8082, and moves the app to another port. From here on, every request you make passes through Keploy on its way in, while the eBPF probe watches the app's database calls on the way out.
Watch out
Quote $PATH. The docs write it as PATH=$PATH, unquoted. On WSL that breaks: your inherited Windows PATH contains directories with spaces (/mnt/c/Program Files/...), bash splits on them, and sudo tries to run Files/Python311/Scripts/:/mnt/c/Program as the command.
Why PATH needs to be passed at all
When you type keploy, the shell searches a list of directories in order and runs the first match. That list is PATH, and Keploy's installer put the binary in one of them — usually /usr/local/bin.
sudo deliberately ignores your PATH and substitutes a fixed, trusted list called secure_path, set in /etc/sudoers. The reason is security: if sudo inherited your PATH, anyone who placed a malicious keploy earlier in it would get it executed as root.
-E doesn't override this. It preserves most environment variables, but secure_path is applied afterwards and specifically wins on PATH. That surprises people, which is why both forms of the command exist in the wild.
So if secure_path doesn't include /usr/local/bin:
keploy --version # works
sudo keploy --version # command not foundSame binary, same machine — different list searched. Passing PATH="$PATH" says "use mine instead," which is a reasonable trade on your own laptop and less so on a shared server.
Check whether it affects you:
echo $PATH
sudo printenv PATH
which keployNote
The README omits PATH entirely and the quickstart includes it unquoted. If sudo keploy already works on your machine, the short form is fine.
You may also see preserving the entire environment is not supported, '-E' is ignored — that's sudoers refusing -E. Harmless; the flag simply does nothing, so it's left out here.
Make some API calls
With recording running, open a second terminal and hit the API. This tutorial uses curl; Postman or Hoppscotch work just as well.
curl --request POST \
--url http://localhost:8082/url \
--header 'content-type: application/json' \
--data '{"url": "https://github.com"}'You'll get back a shortened URL:
{"ts":1788079291093424896,"url":"http://localhost:8082/4KepjkTT"}Paste that into a browser and it redirects to the original — a second request, captured as a second test case.
Stop recording with Ctrl+C, then look at what Keploy wrote:
ls -R keploy/keploy/test-set-1:
mocks.yaml tests
keploy/test-set-1/tests:
get-4kepjktt-1.yaml post-url-1.yaml
Each API call became a test case — the request you sent and the response you got. The Postgres exchanges behind them went into mocks.yaml. That pairing is the whole idea: the test cases are what gets replayed, and the mocks are what answers the database calls when it does.
Note
test-set-0 ships with the repo — it isn't yours. Your recording created test-set-1. This matters in the next step.
Step 8 — Run the tests
sudo PATH="$PATH" keploy test -c "./echo-psql-url-shortener" --delay 10 --test-sets test-set-1--delay 10 gives the app ten seconds to start before Keploy begins replaying.
Gotcha
If a test run fails before your recorded test cases execute, check the existing test sets and mocks first. The sample repository may already contain test data that has nothing to do with the tests you just recorded.
--test-sets test-set-1 is not optional here. The quickstart's command
omits it, which runs everything — including the test-set-0 that ships with
the repo. Its mocks are in an older format that 3.8.2 can't read, so they
fail to unmarshal, the app's database ping gets an EOF, the app exits, and
the whole run dies before it ever reaches your tests.
The errors point at an unreachable app and connectivity problems. Neither is the cause. Naming your own test set skips it entirely.
Step 9 — Read the output
<=========================================>
COMPLETE TESTRUN SUMMARY.
Total tests: 2
Total test passed: 2
Total test failed: 0
Total time taken: "10.09 s"
<=========================================>
Both requests replayed and matched. The line worth noticing is further up:
IT metrics successfully sent {"testsRan": 2, "mocksConsumed": 4}
Four mocks consumed. The app pinged the database, created its table, and ran both queries — and Postgres answered none of it. Every one of those calls was served from the recording. That's the entire value proposition visible in a single line: no live database, no hand-written mocks, and the app never knew the difference.
Note
After a fully green run you'll still see a red line:
ERROR failed to read the test-set report {"reportName": "test-set-0-report", ...}
and a summary that says "across 2 test-set(s)" when only one ran. --test-sets filters which tests execute, but the discovery and reporting stages still work from the full list. Cosmetic — your results are correct.
Where this fits
For me, Keploy is most useful when you already have a working service and want regression coverage without maintaining a large set of database mocks by hand. The recorded traffic gives you a practical starting point, but it doesn't replace deliberately testing paths you haven't exercised — if you never send a request through a particular part of the application, Keploy can't record a test for it.