Keploy + Go — a walkthrough

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.04

Then 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.sh

It will ask for your password. Nothing appears as you type — that's normal.

Verify:

keploy --version

If yours isn't 3.8.2, some flags below may have changed.

Step 2 — Log in

sudo keploy login

Gotcha

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-login

Step 3 — Get the sample app

git clone https://github.com/keploy/samples-go.git && cd samples-go/echo-sql
go mod download

go 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 postgres

The 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 ps

That 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.go

Or view the top of the file with line numbers:

cat -n main.go | head -50

Edit it with nano main.go, then confirm the change:

git diff main.go

Note

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 -cover

Note

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 found

Same 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 keploy

Note

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.