Mockly — Go Client¶
The Go client starts, controls, and stops a Mockly process from within your Go tests.
Install¶
Quickstart¶
package mypackage_test
import (
"testing"
mocklydriver "github.com/dever-labs/mockly/clients/go"
)
func TestMyService(t *testing.T) {
// Install binary if needed, then start the server
server, err := mocklydriver.Ensure(
mocklydriver.Options{},
mocklydriver.InstallOptions{},
)
if err != nil {
t.Fatalf("start mockly: %v", err)
}
defer server.Stop()
err = server.AddMock(mocklydriver.Mock{
ID: "get-user",
Request: mocklydriver.Request{Method: "GET", Path: "/users/1"},
Response: mocklydriver.Response{
Status: 200,
Body: `{"id":1,"name":"Alice"}`,
Headers: map[string]string{"Content-Type": "application/json"},
},
})
if err != nil {
t.Fatalf("add mock: %v", err)
}
// Point your service under test at server.HTTPBase, then assert responses
_ = server.HTTPBase // e.g. "http://127.0.0.1:45678"
}
Factory methods¶
| Function | Description |
|---|---|
mocklydriver.Ensure(opts, installOpts) |
Downloads the binary if not present, then starts the server. Recommended for most cases. |
mocklydriver.Create(opts) |
Starts the server using an already-installed binary. Fails immediately if the binary cannot be found. |
Both retry up to 3 times on ephemeral port conflicts.
Configuration¶
// With pre-loaded scenarios
server, err := mocklydriver.Ensure(
mocklydriver.Options{
Scenarios: []mocklydriver.Scenario{
{
ID: "payment-fail",
Name: "Payment Failure",
Patches: []mocklydriver.ScenarioPatch{
{MockID: "charge", Status: ptr(503), Body: ptr(`{"error":"unavailable"}`)},
},
},
},
},
mocklydriver.InstallOptions{},
)
API reference¶
Mocks¶
statusCreated := 201
emptyBody := `[]`
retryDelay := "250ms"
// Add a mock
err = server.AddMock(mocklydriver.Mock{
ID: "get-orders",
Request: mocklydriver.MockRequest{
Method: "GET",
Path: "/orders",
Headers: map[string]string{"Authorization": "Bearer *"},
},
Response: mocklydriver.MockResponse{
Status: 200,
Body: `[{"id":1}]`,
Headers: map[string]string{"Content-Type": "application/json"},
Delay: "100ms",
},
})
// Inspect the currently registered mocks
mocks, err := server.ListMocks()
if err != nil {
t.Fatalf("list mocks: %v", err)
}
_ = mocks
// Replace a mock definition
updated, err := server.UpdateMock("get-orders", mocklydriver.Mock{
ID: "get-orders",
Request: mocklydriver.MockRequest{Method: "GET", Path: "/orders"},
Response: mocklydriver.MockResponse{
Status: 200,
Body: `[{"id":1},{"id":2}]`,
Headers: map[string]string{"Content-Type": "application/json"},
},
})
if err != nil {
t.Fatalf("update mock: %v", err)
}
_ = updated
// Patch only the response fields you want to change
patched, err := server.PatchMock("get-orders", mocklydriver.MockResponsePatch{
Status: &statusCreated,
Body: &emptyBody,
Headers: map[string]string{
"X-Mock-Version": "v2",
},
Delay: &retryDelay,
})
if err != nil {
t.Fatalf("patch mock: %v", err)
}
_ = patched
// Remove a mock
err = server.DeleteMock("get-orders")
Scenarios¶
scenarioStatus := 503
scenarioDelay := "750ms"
createdScenario, err := server.CreateScenario(mocklydriver.Scenario{
ID: "slow-checkout",
Name: "Slow checkout",
Description: "Used for retry-path tests",
Patches: []mocklydriver.ScenarioPatch{
{
MockID: "charge",
Status: &scenarioStatus,
Delay: &scenarioDelay,
},
},
})
if err != nil {
t.Fatalf("create scenario: %v", err)
}
_ = createdScenario
scenarios, err := server.ListScenarios()
if err != nil {
t.Fatalf("list scenarios: %v", err)
}
_ = scenarios
loadedScenario, err := server.GetScenario("slow-checkout")
if err != nil {
t.Fatalf("get scenario: %v", err)
}
loadedScenario.Name = "Slow checkout v2"
updatedScenario, err := server.UpdateScenario("slow-checkout", *loadedScenario)
if err != nil {
t.Fatalf("update scenario: %v", err)
}
_ = updatedScenario
// Activate a scenario before exercising your service
err = server.ActivateScenario("slow-checkout")
if err != nil {
t.Fatalf("activate scenario: %v", err)
}
activeScenarios, err := server.ListActiveScenarios()
if err != nil {
t.Fatalf("list active scenarios: %v", err)
}
_ = activeScenarios.Active
// Deactivate or delete it when you're done
err = server.DeactivateScenario("slow-checkout")
if err != nil {
t.Fatalf("deactivate scenario: %v", err)
}
err = server.DeleteScenario("slow-checkout")
Call verification¶
summary, err := server.WaitForCalls("get-orders", 2, 5)
if err != nil {
t.Fatalf("wait for calls: %v", err)
}
if summary.Count != 2 {
t.Fatalf("expected 2 calls, got %d", summary.Count)
}
latestCalls, err := server.GetCalls("get-orders")
if err != nil {
t.Fatalf("get calls: %v", err)
}
_ = latestCalls.Calls
err = server.ClearCalls("get-orders")
if err != nil {
t.Fatalf("clear calls: %v", err)
}
err = server.ClearAllCalls()
State¶
state, err := server.GetState()
if err != nil {
t.Fatalf("get state: %v", err)
}
_ = state["order-status"]
updatedState, err := server.SetState(map[string]string{
"order-status": "pending",
"retry-count": "1",
})
if err != nil {
t.Fatalf("set state: %v", err)
}
_ = updatedState["retry-count"]
err = server.DeleteState("retry-count")
Logs¶
allLogs, err := server.GetLogs("")
if err != nil {
t.Fatalf("get logs: %v", err)
}
matchedLogs, err := server.GetLogs("get-orders")
if err != nil {
t.Fatalf("get filtered logs: %v", err)
}
_ = allLogs
_ = matchedLogs
totalLogs, err := server.GetLogsCount("")
if err != nil {
t.Fatalf("count logs: %v", err)
}
matchedCount, err := server.GetLogsCount("get-orders")
if err != nil {
t.Fatalf("count filtered logs: %v", err)
}
_ = totalLogs
_ = matchedCount
err = server.ClearLogs()
Fault injection¶
// Add latency and override status codes on all requests
server.SetFault(mocklydriver.FaultConfig{
Enabled: true,
Delay: "500ms",
Status: 503,
ErrorRate: 0.5, // 50% of requests
})
// Remove the fault
server.ClearFault()
Reset and stop¶
// Reset all dynamic mocks, active scenarios, and faults; keeps startup config
server.Reset()
// Kill the process
server.Stop()
Integration with testing.T¶
TestMain (shared server for a package)¶
package mypackage_test
import (
"os"
"testing"
mocklydriver "github.com/dever-labs/mockly/clients/go"
)
var mockly *mocklydriver.Server
func TestMain(m *testing.M) {
var err error
mockly, err = mocklydriver.Ensure(mocklydriver.Options{}, mocklydriver.InstallOptions{})
if err != nil {
panic(err)
}
code := m.Run()
mockly.Stop()
os.Exit(code)
}
func TestGetUser(t *testing.T) {
defer mockly.Reset()
mockly.AddMock(mocklydriver.Mock{
ID: "get-user",
Request: mocklydriver.Request{Method: "GET", Path: "/users/1"},
Response: mocklydriver.Response{Status: 200, Body: `{"id":1}`},
})
// ... test your service ...
}
Per-test server¶
func TestIsolated(t *testing.T) {
server, err := mocklydriver.Ensure(mocklydriver.Options{}, mocklydriver.InstallOptions{})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { server.Stop() })
// ... test ...
}
Server properties¶
| Field | Description |
|---|---|
server.HTTPBase |
Base URL of the mock HTTP server, e.g. http://127.0.0.1:45123 |
server.APIBase |
Base URL of the management API, e.g. http://127.0.0.1:45124 |
server.HTTPPort |
Numeric HTTP port |
server.APIPort |
Numeric API port |
Testcontainers¶
Mockly also ships a Docker-backed Go testcontainers module in the same repository: github.com/dever-labs/mockly/clients/go/testcontainers.
Use it instead of the driver when you want Docker-managed lifecycle, no local binary download, and the same container image in local tests and CI.
Install¶
go get github.com/dever-labs/mockly/clients/go/testcontainers
go get github.com/testcontainers/testcontainers-go
Example¶
package mypackage_test
import (
"context"
"io"
"net/http"
"testing"
mocklydriver "github.com/dever-labs/mockly/clients/go"
testcontainersmockly "github.com/dever-labs/mockly/clients/go/testcontainers"
)
func TestReturnsUserFromContainer(t *testing.T) {
ctx := context.Background()
container, err := testcontainersmockly.Run(ctx)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = container.Terminate(context.Background()) })
if err := container.AddMock(ctx, mocklydriver.Mock{
ID: "get-user",
Request: mocklydriver.MockRequest{Method: http.MethodGet, Path: "/users/1"},
Response: mocklydriver.MockResponse{Status: 200, Body: `{"id":1}`},
}); err != nil {
t.Fatal(err)
}
httpBase, _ := container.HTTPBase(ctx)
resp, err := http.Get(httpBase + "/users/1")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if got := string(body); resp.StatusCode != 200 || got != `{"id":1}` {
t.Fatalf("unexpected response: status=%d body=%s", resp.StatusCode, got)
}
}
Key API¶
Run(ctx, opts...)to start the containerWithImage(image)andWithInlineConfig(yaml)startup optionsHTTPBase(ctx)/APIBase(ctx)AddMock,DeleteMock,ResetActivateScenario,DeactivateScenarioSetFault,ClearFault
Requirements¶
- Go 1.21+
- Docker