Skip to content

Testing with testkit

pkg/testkit provides test infrastructure so you can test modules without spinning up a full service.

NewHarness creates a context with a DI injector, suitable for testing a single module’s Init in isolation.

func TestMyModule_Init(t *testing.T) {
h := testkit.NewHarness(t)
// Provide config if the module is Configurable
h = h.WithData(map[string]any{
"modules.mymodule.default.host": "localhost",
"modules.mymodule.default.port": 8080,
})
// Provide any DI dependencies the module needs
testkit.WithProvider[*pgxpool.Pool](h, func(do.Injector) (*pgxpool.Pool, error) {
return fakePool, nil
})
m := mymodule.NewModule()
testza.AssertNoError(t, m.Init(h.Ctx()))
// Invoke what the module registered
svc := do.MustInvoke[*mymodule.MyService](lakta.GetInjector(h.Ctx()))
testza.AssertNotNil(t, svc)
}

NewRuntimeHarness starts a full Runtime in a goroutine and gives you a Shutdown() to stop it cleanly.

func TestMyModule_Start(t *testing.T) {
rh := testkit.NewRuntimeHarness(t,
config.NewModule(),
slog.NewModule(),
mymodule.NewModule(),
)
defer rh.Shutdown()
// runtime is now running; test behavior
// e.g. make an HTTP request, invoke a gRPC method
}

Use mock modules to satisfy the runtime when you don’t need real implementations:

mock := testkit.NewMockModule() // basic Module
syncMock := testkit.NewMockSyncModule() // SyncModule, has BlockStart chan
asyncMock := testkit.NewMockAsyncModule()

MockSyncModule.BlockStart is a chan struct{} — close it to unblock Start:

go func() {
time.Sleep(100 * time.Millisecond)
close(syncMock.BlockStart)
}()
h := testkit.NewHarness(t).WithData(initialData)
notifier := do.MustInvoke[*config.ReloadNotifier](lakta.GetInjector(h.Ctx()))
notifier.FireReload(newKoanf) // triggers all registered OnReload callbacks

Always use testza:

testza.AssertNoError(t, err)
testza.AssertNotNil(t, result)
testza.AssertEqual(t, expected, actual)