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

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

One Go test function runs all of them:

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

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

go
[
  {
    "serviceName": "ShipmentTracking",
    "httpMethodType": "POST",
    "servicePath": "/api/v1/shipments/track",
    "filePath": "testdata/shipments",
    "scenariosFileName": "shipment_tracking_scenarios.json",
    "handlerName": "HandlerShipmentTracking"
  }
]

Scenario JSON schema

go
{
  "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

FieldTypeDescription
namestringRequired. Test name (shown in go test -v output)
descriptionstringHuman-readable description
requestMethodstringHTTP method. Default: GET
requestUrlstringRequired. URL path to call (e.g. /api/v1/users)
requestFileNamestringPath to request body JSON file (relative to scenario dir)
responseFileNamestringPath to expected response JSON file (relative to scenario dir)
expectedCodeintRequired. Expected HTTP status code
isMockRequiredboolIf true, any un-mocked outgoing call fails the test
isDbMockedboolInformational flag — reserved for DB mock wiring
headersobjectExtra request headers (e.g. auth tokens)
netUtilMockSteparrayList of mock steps

Mock steps

HTTP request mock (method: "httprequest")

Intercepts outgoing calls made via pkg/http. Matched by URL prefix.

go
{
  "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:

MethodIntercepts
sendmailpkg/mail sends
smsSMS/notification sends
notificationPush notification sends
go
{ "method": "sendmail", "isMock": true, "returnData": { "body": "" } }

Custom function mock

Register your own mocker once in a test init:

go
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

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

  1. Load scenario JSON
  2. Read request body from requestFileName
  3. Install HTTP mock transport (MockTransport)
  4. Activate function mocks (sendmail, sms, …)
  5. Fire request against handler via httptest
  6. Assert HTTP status code
  7. JSON deep-diff actual vs expected response
  8. Verify all isMock: true steps were called
  9. Reset all mocks

Advanced: testify mock expectations

Access the underlying testify/mock.Mock for custom assertions:

go
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

AssertionBehaviour
Status codetestify/assert.Equal — prints expected vs actual
Response bodyJSON normalised (key order / whitespace ignored), testify/assert.Equal
HTTP mocks calledFails per un-triggered isMock: true httprequest step
Func mocks calledFails per un-triggered isMock: true func step

Debugging

Print a scenario summary to stdout:

go
s, _ := testkit.LoadScenario("testdata/create_user.json")
testkit.DumpScenario(s)

Output:

go
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=""