Keploy + Go quickstart
Record, Replay, Relax: API Tests for Go with Keploy
Turn real API calls from a Gin + MongoDB app into tests and database mocks with Keploy.
- ~25 min
- Beginner
- Windows + WSL
- Keploy 3.8.57
What you'll learn
TL;DR
Already have WSL 2, Docker Desktop, Go and Keploy installed and logged in? This is the core flow:
git clone --depth 1 https://github.com/keploy/samples-go.git && cd samples-go/gin-mongo
mv keploy keploy-original && sed -i 's/"mongoDb:27017"/"localhost:27017"/' main.go
docker compose up -d mongo
keploy record -c "go run main.go handler.go" # Terminal 1; Ctrl+C when done
curl -X POST http://localhost:8080/url -H 'content-type: application/json' -d '{"url": "https://google.com"}' # Terminal 2
keploy test -c "go run main.go handler.go" --delay 10New to Keploy, or something broke? Follow the full steps below.
Why Keploy?
Testing a Gin handler that talks to MongoDB usually means writing httptest code plus hand-made mocks for the database. Those mocks are written from memory, so over time they drift from what MongoDB actually returns.
Keploy takes a different route. It sits next to your app, captures each API request and the database calls that request triggers, and saves both as YAML: test cases plus mocks. Later it replays the requests and answers the database calls from the mocks.
There's no SDK and no code change. You wrap the command you already use to start the app:
keploy record -c "go run main.go handler.go"How it works
You use the app normally; Keploy captures everything.
Keploy replays the saved requests; no database required.
- Record mode: Keploy captures every request and the MongoDB traffic it causes, and writes them to a
keploy/folder. - Test mode: Keploy replays the recorded requests, serves MongoDB responses from the mocks, and compares each new response with the recorded one.
- Noise: fields that change on every run, such as timestamps, are left out of the comparison so they can't cause false failures.
Prerequisites
This is the setup I tested on. Newer versions should work, but your output may differ slightly from mine.
| Component | Version |
|---|---|
| OS | Windows 11 + WSL 2 (Ubuntu 24.04, kernel 6.18) |
| Docker | Docker Desktop 29.7.2 with WSL integration |
| Go | 1.27.1 |
| Git | 2.43 |
| Keploy | 3.8.57 (Free plan) |
| Sample app | keploy/samples-go → gin-mongo, a URL shortener built with Gin + MongoDB |
Not on Windows? On Linux, skip Step 1 and run everything in your normal terminal. On macOS, follow Keploy's installation guide (opens in a new tab) for setup, then continue from Step 4.
Keploy needs a Linux kernel of 5.10 or newer. Check yours in Ubuntu (mine is 6.18):
uname -rKeploy's current install docs say it also runs natively on Windows (x86-64), without WSL. I used WSL anyway: the Linux path is the classic, best-documented route, and my MongoDB runs in Docker.
Step 1: Prepare WSL and Docker
Keploy and the Go app will run inside Ubuntu, and MongoDB will run in Docker. Both need to see each other, so set that up first.
Check that Ubuntu runs on WSL 2
In PowerShell:
PowerShellwsl -l -vThe
VERSIONcolumn for your Ubuntu distro must say2. WSL 1 doesn't run a real Linux kernel, and Keploy needs one to watch network traffic.Turn on Docker Desktop's WSL integration
In Docker Desktop, open Settings → Resources → WSL integration, enable it for your Ubuntu distro, then click Apply & restart. This makes the
dockercommand available inside Ubuntu.Test Docker from Ubuntu
Bashdocker run hello-worldIf this prints a hello message instead of "command not found", Docker works from WSL.
Pasting with Ctrl + V in the Ubuntu terminal inserted a hidden ^[[200~ in front of my command and broke it. Paste with right-click or Ctrl + Shift + V instead.
Step 2: Install Go
Ubuntu's apt offers Go 1.21, which is several releases behind. Install the official release from go.dev (opens in a new tab) instead:
GO_VERSION=go1.27.1
wget https://go.dev/dl/${GO_VERSION}.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf ${GO_VERSION}.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> ~/.bashrc
source ~/.bashrc
go versionThe rm -rf removes any older Go first, because extracting over an existing install can leave stale files behind. The PATH line adds the go binary and $HOME/go/bin, where go install puts tools.
go version go1.27.1 linux/amd64
Step 3: Install Keploy and log in
curl --silent -O -L https://keploy.io/install.sh && source install.shMy install printed v3.8.57, while the sample output in the docs shows 3.4.1. Don't worry if your output looks different from the docs.
On my machine, the installer also created a Docker network called keploy-network. The long hex ID it printed right after the progress bar is that network's ID (docker network ls shows it). You'll need this network in Step 5.
The installer also suggests keploy mcp-install, which sets Keploy up for AI coding agents. It's optional; skip it for this tutorial.
Next, log in so the CLI is linked to your Keploy account. The docs use the browser login; on WSL, the API key login is what worked for me:
keploy loginThe sign-in link is valid for 1 minute. On WSL, no browser opened, and even after I signed in manually, the terminal timed out twice with "invalid or expired code".
What worked: create an API key at app.keploy.io (opens in a new tab) (profile → Account Settings → API Keys → Generate new token), then run keploy login --manual-login and paste the key.
When the key is accepted, you'll see:
API key verified successfully! You are on the Keploy Free plan.
The login prompt echoes the key, so never screenshot or share that terminal output. If the key leaks, delete the token at app.keploy.io (opens in a new tab) and create a new one.
Step 4: Get the sample app
git clone --depth 1 https://github.com/keploy/samples-go.git
cd samples-go/gin-mongo
go mod download--depth 1 skips the repository's full history, which makes the clone much faster. go mod download fetches the dependencies now, so the first recorded run doesn't stall on downloads.
- gin-mongo/
- Dockerfile
- docker-compose.yaml
- go.mod
- handler.go
- main.go
- keploy/pre-recorded by the repo
Before recording, two things need fixing.
Clear the old recordings
The repo ships an old keploy/ folder with someone else's recordings. Move it aside so your run starts clean:
mv keploy keploy-originalPoint the app at localhost
Line 35 of main.go connects to mongoDb:27017. mongoDb is a container name, which only resolves inside Docker's network. In this tutorial, the Go app runs directly in Ubuntu, so it has to use localhost:27017 (Docker publishes MongoDB's port to the host).
sed -i 's/"mongoDb:27017"/"localhost:27017"/' main.go- client, err := New("mongoDb:27017", dbName)
+ client, err := New("localhost:27017", dbName)The docs say this is on line 21; in the current repo it's line 35.
Step 5: Start MongoDB
docker compose up -d mongo
docker psIn the docker ps output, the MongoDB container's PORTS column should show 0.0.0.0:27017->27017/tcp. That port mapping is what lets the app reach localhost:27017.
If Docker Compose warns that the attribute version is obsolete, ignore it. The warning is harmless.
docker-compose.yaml expects an external Docker network named keploy-network, which the Keploy installer created on my machine. If Docker Compose says the network is not found, create it and run docker compose up -d mongo again:
docker network create keploy-networkStep 6: Record test cases
You'll need two Ubuntu terminals, both in samples-go/gin-mongo.
Terminal 1: Start the app under Keploy
Bashkeploy record -c "go run main.go handler.go"Keploy asks for your sudo password. It needs elevated rights to watch the app's network traffic.
Wait before sending requestsWait for the line
Started ingress forwarding {"orig_port": 8080, ...}. On my first attempt, no requests reached the app while recording, and Keploy saved an empty test set (testsRan: 0).Terminal 2: Send requests
Shorten a URL:
Bashcurl --request POST \ --url http://localhost:8080/url \ --header 'content-type: application/json' \ --data '{"url": "https://google.com"}'Response{"ts":1790866863265379939,"url":"http://localhost:8080/Lhr4BWAi"}The JSON has no trailing newline, so it sticks to your next prompt. The first time, I thought the request had printed nothing and re-sent it with
curl -vto inspect it. That's why my recording has two POSTs forhttps://google.com(post-url-1andpost-url-2). Look for the JSON right before your prompt.The short code is derived from the URL, so
https://google.comalways givesLhr4BWAi(the same code as in the docs), whiletschanges every time.Now follow the short link. The app answers with a 303 redirect to
https://google.com:Bashcurl http://localhost:8080/Lhr4BWAiRepeat both calls with a second URL. For
https://keploy.io, the response contained the short code7fvpSsFg:Bashcurl --request POST \ --url http://localhost:8080/url \ --header 'content-type: application/json' \ --data '{"url": "https://keploy.io"}' curl http://localhost:8080/7fvpSsFgIf you run exactly these commands, you'll get 4 tests; my run has 5 because of the re-sent POST, so expect 4/4 in Step 8 and 2 passed / 2 failed in Step 10.
Terminal 1: Watch the captures
Each request shows up as a
[GIN]line followed by a capture message from Keploy:keploy record[GIN] 2026/10/01 - 14:58:44 | 200 | 21.646234ms | 127.0.0.1 | POST "/url" 🐰 Keploy: ... INFO 🟠 Keploy has captured test cases for the user's application. {"path": ".../keploy/test-set-0/tests", "testcase name": "post-url-1"} ... [GIN] 2026/10/01 - 15:02:26 | 303 | 3.874109ms | 127.0.0.1 | GET "/Lhr4BWAi" 🐰 Keploy: ... INFO 🟠 Keploy has captured test cases for the user's application. {"path": ".../keploy/test-set-0/tests", "testcase name": "get-lhr4bwai-1"}The test names are descriptive (
post-url-1,get-lhr4bwai-1) instead of the docs'test-1.
Step 7: Stop recording (and meet auto-replay)
Press Ctrl + C in Terminal 1.
Keploy 3.8.57 doesn't stop right away. It automatically replays what it just recorded to find fields that change between runs (noise):
🐰 Keploy: ... INFO auto-replay: recording stopped — now replaying what was just recorded to check for noise. Press Ctrl+C again to skip and exit. {"test_sets": 1, "opt_out": "--disableAutoReplay, or keploy.yml record.disableAutoReplay, to skip this on every run"}
...
🐰 Keploy: ... INFO persisted learned request-body noise onto mocks {"testSetID": "test-set-0", "mocksWithLearnedNoise": 3, "totalMocks": 10}
🐰 Keploy: ... INFO Successfully saved test-mock mappings {"testSetID": "test-set-0", "numTests": 5, "startupMocks": 0}
<=========================================>
TESTRUN SUMMARY. For test-set: "test-set-0"
Total tests: 5
Total test passed: 5
Total test failed: 0
Time Taken: "15.07 s"
<=========================================>All 5 recorded tests passed on replay. To skip this step, press Ctrl + C again, add --disableAutoReplay to the record command, or set record.disableAutoReplay in keploy.yml to skip it on every run.
Keploy generated this:
- keploy/
- reports/
- test-run-0/
- test-set-0-report.yaml
- test-set-0/
- config.yaml
- mappings.yaml
- mocks.yaml
- tests/
- get-7fvpssfg-1.yaml
- get-lhr4bwai-1.yaml
- post-url-1.yaml
- post-url-2.yaml
- post-url-3.yaml
tests/ holds one YAML file per request and mocks.yaml holds the recorded MongoDB traffic. I noticed that Keploy lowercased the path in the test name: /7fvpSsFg became get-7fvpssfg-1. Judging by the log line "Successfully saved test-mock mappings" (numTests: 5), mappings.yaml links each test case to the mocks it uses.
Here's a recorded test case, trimmed to the interesting parts:
version: api.keploy.io/v1beta1
kind: Http
name: post-url-1
spec:
req:
method: POST
url: http://localhost:8080/url
body: '{"url": "https://google.com"}'
resp:
status_code: 200
body: '{"ts":1790866724945749450,"url":"http://localhost:8080/Lhr4BWAi"}'
assertions:
noise:
body.ts: []
header.Date: []The highlighted noise block is the result of auto-replay. The docs tell you to add body.ts to noise by hand; in 3.8.57, Keploy detected body.ts and header.Date by itself. Each test file also contains a ready-to-run curl command that reproduces the request.
Following Keploy's own guidance: commit tests/ and config.yaml, but not mocks.yaml. Mocks can get large, so Keploy gitignores mocks.yaml and uploads mocks to its registry instead.
Step 8: Run the tests
In Terminal 1 (still in samples-go/gin-mongo), run the tests. MongoDB is still running for now:
keploy test -c "go run main.go handler.go" --delay 10--delay 10 gives the app 10 seconds to boot before Keploy starts replaying requests. Without it, the first requests could hit an app that isn't listening yet.
Step 9: Prove the mocks work by turning MongoDB off
If the tests really use the mocks, they shouldn't care whether MongoDB is running. Stop it and test again:
docker compose stop mongo
keploy test -c "go run main.go handler.go" --delay 10All 5 tests still pass, and Keploy's summary calls the suite "dependency-free".
In test mode, the app believes it's talking to MongoDB, but Keploy answers with the recorded responses. That means these tests can run in CI without spinning up a database.
Start MongoDB again before you continue:
docker compose start mongoStep 10: Catch a real regression
Imagine a teammate "improves" the redirect from 303 See Other to 301 Moved Permanently. It compiles and looks harmless. But browsers cache 301 redirects, so if a short link is ever changed, users keep landing on the old URL.
The redirect is on line 66 of handler.go. Introduce the change:
sed -i '66s/http.StatusSeeOther/http.StatusMovedPermanently/' handler.go
keploy test -c "go run main.go handler.go" --delay 10- c.Redirect(http.StatusSeeOther, u.URL)
+ c.Redirect(http.StatusMovedPermanently, u.URL)Keploy flags the difference:
Only the two GET (redirect) tests failed; the three POST tests passed. Keploy points at exactly the endpoints whose behavior changed.
Keploy's output also warns that noise was already calibrated at record time, so these are real differences. Don't hide them by marking them as noise. A failing test is information, not something to silence.
Revert the change and run the tests again to get back to 5/5:
git checkout handler.go
keploy test -c "go run main.go handler.go" --delay 10Clean up
When you're done, stop and remove the MongoDB container (run this in samples-go/gin-mongo):
docker compose downThe keploy/ folder with your tests stays on disk, so you can run keploy test again later, after docker compose up -d mongo or even without MongoDB.
My takeaways
Stopping MongoDB and still seeing 5 out of 5 tests pass. That's when it clicked for me: the tests no longer depend on a real database, which is exactly what makes running them in CI simple.
Not Keploy itself, but the environment around it. The browser login timed out on WSL until I switched to an API key, and my first recording saved zero tests because I sent requests before Keploy was ready. The lesson: read the logs, and wait for Started ingress forwarding before sending traffic.
Yes, for API-heavy Go services, as a regression safety net next to normal unit tests. Recording real traffic took minutes, and it caught a one-word bug (303 to 301) that would easily pass code review. I'd still review what gets marked as noise and keep mocks.yaml out of Git, as Keploy recommends.
What's different in Keploy 3.8.57
If you follow the official quickstart alongside this guide, here's where my run with Keploy 3.8.57 differed:
| Topic | Quickstart docs | What I saw |
|---|---|---|
| Version in sample output | 3.4.1 | 3.8.57 |
| Login | Browser login | Browser login timed out on WSL; --manual-login with an API key worked |
MongoDB address in main.go | Line 21 | Line 35 |
| Generated files | test-1.yml + mocks.yml | config.yaml, mappings.yaml, mocks.yaml, and tests/ with descriptive names |
| Noise | Add body.ts manually | Detected automatically by auto-replay |
| After recording | Nothing mentioned | Automatic replay plus a link to a cloud test report |
Troubleshooting
Where to go next
- Run
keploy testin CI on every pull request. Keploy's CLI suggests installing Keploy with the install script in CI, storingKEPLOY_API_KEYas a secret, and committingtests/andconfig.yaml. - Record more endpoints. Every new request you send while recording becomes another test case.
Useful links: