TestKit — JSON-Scenario-Driven API Testing
pkg/testkit lets you write REST API integration tests entirely in JSON. One JSON file = one test case. No repeated Go boilerplate.
It is powered by testify — testify/assert for assertions and testify/mock for mocking side-effects.
Concept
testdata/
create_user.json ← scenario (what to do & assert)
create_user_req.json ← request body
create_user_res.json ← expected response body
health_check.json ← another scenarioOne Go test function runs all of them:
func TestAPI(t *testing.T) {
handler := kernel.NewHTTPKernel().Handler()
testkit.RunDir(t, handler, "testdata")
}Data-Driven Test Suites
To execute multiple APIs spanning different isolated environments and handler overrides, RunSuite provides a master configuration approach driven entirely through JSON.
func TestSuiteRun(t *testing.T) {
// A map translating string identifiers in your config map into live Handler pointers
handlers := map[string]http.HandlerFunc{
"HandlerShipmentTracking": api.ShipmentTrackingController,
"HandlerBillingProcessing": api.BillingController,
}
// Loads and executes the testing Master Config definition JSON
testkit.RunSuite(t, "testdata/test_scenarios_master.json", handlers)
}Master Configuration Schema
The master file defines arrayed routes mapping URLs to handlers and scenario sets.
[
{
"serviceName": "ShipmentTracking",
"httpMethodType": "POST",
"servicePath": "/api/v1/shipments/track",
"filePath": "testdata/shipments",
"scenariosFileName": "shipment_tracking_scenarios.json",
"handlerName": "HandlerShipmentTracking"
}
]Scenario JSON schema
{
"name": "Create User",
"description": "POST /api/v1/users returns 201",
"requestMethod": "POST",
"requestUrl": "/api/v1/users",
"requestFileName": "create_user_req.json",
"responseFileName": "create_user_res.json",
"expectedCode": 201,
"isMockRequired": true,
"isDbMocked": false,
"headers": {
"Authorization": "Bearer test-token"
},
"netUtilMockStep": [
{
"method": "httprequest",
"isMock": true,
"matchUrl": "https://verify.external.com/",
"returnData": { "statusCode": 200, "body": "eyJ2ZXJpZmllZCI6dHJ1ZX0=" }
},
{
"method": "sendmail",
"isMock": true,
"returnData": { "body": "" }
}
]
}Field reference
| Field | Type | Description |
|---|---|---|
name | string | Required. Test name (shown in go test -v output) |
description | string | Human-readable description |
requestMethod | string | HTTP method. Default: GET |
requestUrl | string | Required. URL path to call (e.g. /api/v1/users) |
requestFileName | string | Path to request body JSON file (relative to scenario dir) |
responseFileName | string | Path to expected response JSON file (relative to scenario dir) |
expectedCode | int | Required. Expected HTTP status code |
isMockRequired | bool | If true, any un-mocked outgoing call fails the test |
isDbMocked | bool | Informational flag — reserved for DB mock wiring |
headers | object | Extra request headers (e.g. auth tokens) |
netUtilMockStep | array | List of mock steps |
Mock steps
HTTP request mock (method: "httprequest")
Intercepts outgoing calls made via pkg/http. Matched by URL prefix.
{
"method": "httprequest",
"isMock": true,
"matchUrl": "https://api.stripe.com/",
"returnData": {
"statusCode": 200,
"body": "eyJpZCI6ImNoXzEyMyJ9"
}
}matchUrl— prefix match. Empty string matches any URL.returnData.body— base64-encoded response body.returnData.statusCode— defaults to 200.
Function mock (method: "sendmail" / "sms" / "notification")
Intercepts non-HTTP side-effects. Built-in methods:
| Method | Intercepts |
|---|---|
sendmail | pkg/mail sends |
sms | SMS/notification sends |
notification | Push notification sends |
{ "method": "sendmail", "isMock": true, "returnData": { "body": "" } }Custom function mock
Register your own mocker once in a test init:
func init() {
testkit.RegisterMocker("payments", testkit.NewFuncMocker("payments"))
}Then use in JSON: "method": "payments".
Base64 encoding the body
# Encode: {"verified":true}
echo -n '{"verified":true}' | base64
# → eyJ2ZXJpZmllZCI6dHJ1ZX0=Runner API
// Run a single scenario
testkit.Run(t, handler, "testdata/create_user.json")
// Run all *.json files in a directory as subtests
testkit.RunDir(t, handler, "testdata")
// Run array-based scenarios defined in a master configuration via dynamically mapped handlers
testkit.RunSuite(t, "testdata/test_scenarios.json", handlersMap)Lifecycle per scenario
- Load scenario JSON
- Read request body from
requestFileName - Install HTTP mock transport (
MockTransport) - Activate function mocks (
sendmail,sms, …) - Fire request against handler via
httptest - Assert HTTP status code
- JSON deep-diff actual vs expected response
- Verify all
isMock: truesteps were called - Reset all mocks
Advanced: testify mock expectations
Access the underlying testify/mock.Mock for custom assertions:
func TestCreateUser(t *testing.T) {
// Override the sendmail mocker
mailer := testkit.NewFuncMocker("sendmail")
mailer.Mock().On("Intercept", mock.Anything).Return(nil)
testkit.RegisterMocker("sendmail", mailer)
testkit.Run(t, handler, "testdata/create_user.json")
// Assert it was called exactly once
mailer.Mock().AssertNumberOfCalls(t, "Intercept", 1)
}Assertions
| Assertion | Behaviour |
|---|---|
| Status code | testify/assert.Equal — prints expected vs actual |
| Response body | JSON normalised (key order / whitespace ignored), testify/assert.Equal |
| HTTP mocks called | Fails per un-triggered isMock: true httprequest step |
| Func mocks called | Fails per un-triggered isMock: true func step |
Debugging
Print a scenario summary to stdout:
s, _ := testkit.LoadScenario("testdata/create_user.json")
testkit.DumpScenario(s)Output:
Scenario: Create User
POST /api/v1/users → 201
requestFile: create_user_req.json
responseFile: create_user_res.json
isMockRequired: true isDbMocked: false
mockStep[0]: method=httprequest isMock=true matchUrl="https://verify.external.com/"
mockStep[1]: method=sendmail isMock=true matchUrl=""