apptest API

apptest

package

API reference for the apptest package.

S
struct

TestHost

TestHost provides a DI container and test HTTP server.

app/testing/testutil.go:16-21
type TestHost struct

Methods

Close
Method

Close shuts down the test server and runs cleanup functions.

func (*TestHost) Close()
{
	h.Server.Close()
	_ = h.Container.Close()
	for _, f := range h.cleanup {
		f()
	}
}
Cleanup
Method

Cleanup registers a function to run when the host closes.

Parameters

fn func()
func (*TestHost) Cleanup(fn func())
{
	h.cleanup = append(h.cleanup, fn)
}
URL
Method

URL returns the full URL for the given path.

Parameters

path string

Returns

string
func (*TestHost) URL(path string) string
{
	return h.Server.URL + path
}
Get
Method

Get issues a GET request and returns a TestResponse.

Parameters

path string

Returns

func (*TestHost) Get(path string) *TestResponse
{
	resp, err := h.Client.Get(h.URL(path))
	if err != nil {
		return &TestResponse{Error: err}
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	return &TestResponse{
		StatusCode: resp.StatusCode,
		Body:       body,
		Headers:    resp.Header,
	}
}
Post
Method

Post issues a POST request and returns a TestResponse.

Parameters

path string
contentType string
body io.Reader

Returns

func (*TestHost) Post(path string, contentType string, body io.Reader) *TestResponse
{
	resp, err := h.Client.Post(h.URL(path), contentType, body)
	if err != nil {
		return &TestResponse{Error: err}
	}
	defer resp.Body.Close()
	responseBody, _ := io.ReadAll(resp.Body)
	return &TestResponse{
		StatusCode: resp.StatusCode,
		Body:       responseBody,
		Headers:    resp.Header,
	}
}

Fields

Name Type Description
Container *di.Container
Server *httptest.Server
Client *http.Client
cleanup []func()
F
function

NewTestHost

NewTestHost builds dependencies before registering routes against the final container.

Parameters

configure
func(*di.Builder)
register
func(*web.Server, *di.Container) error

Returns

app/testing/testutil.go:24-51
func NewTestHost(configure func(*di.Builder), register func(*web.Server, *di.Container) error) *TestHost

{
	builder := di.NewBuilder()
	server := web.New()
	if configure != nil {
		configure(builder)
	}

	container, err := builder.Build()
	if err != nil {
		panic(err)
	}
	if register != nil {
		if err := register(server, container); err != nil {
			_ = container.Close()
			panic(err)
		}
	}
	testServer := httptest.NewServer(server)

	return &TestHost{
		Container: container,
		Server:    testServer,
		Client:    testServer.Client(),
	}
}
F
function

Resolve

Resolve retrieves a typed dependency from the container.

Parameters

Returns

T
app/testing/testutil.go:103-105
func Resolve[T any](h *TestHost) T

{
	return di.ResolveType[T](h.Container)
}
S
struct

TestResponse

TestResponse holds an HTTP response for test assertions.

app/testing/testutil.go:108-113
type TestResponse struct

Methods

Decode
Method

Decode unmarshals the response body into v.

Parameters

v any

Returns

error
func (*TestResponse) Decode(v any) error
{
	return json.Unmarshal(r.Body, v)
}
String
Method

String returns the response body as a string.

Returns

string
func (*TestResponse) String() string
{
	return string(r.Body)
}

Fields

Name Type Description
StatusCode int
Body []byte
Headers http.Header
Error error
S
struct

FakeLogger

FakeLogger collects log entries for test assertions.

app/testing/testutil.go:126-128
type FakeLogger struct

Methods

AssertLogged
Method

AssertLogged fails the test if msg was not logged.

Parameters

msg string
func (*FakeLogger) AssertLogged(t *testing.T, msg string)
{
	t.Helper()
	found := false
	for _, e := range l.Entries {
		if e == msg {
			found = true
			break
		}
	}
	if !found {
		t.Errorf("expected log %q, not found", msg)
	}
}

Fields

Name Type Description
Entries []string
F
function

NewFakeLogger

NewFakeLogger creates a FakeLogger.

Returns

app/testing/testutil.go:131-133
func NewFakeLogger() *FakeLogger

{
	return &FakeLogger{}
}
S
struct

TestContext

TestContext holds a context and variable map for test use.

app/testing/testutil.go:151-154
type TestContext struct

Fields

Name Type Description
Ctx context.Context
Vars map[string]any
F
function

NewTestContext

NewTestContext creates a TestContext with a background context.

Returns

app/testing/testutil.go:157-162
func NewTestContext() *TestContext

{
	return &TestContext{
		Ctx:  context.Background(),
		Vars: make(map[string]any),
	}
}
S
struct

testService

app/testing/testutil_test.go:13-15
type testService struct

Fields

Name Type Description
Name string
S
struct

closeTracker

app/testing/testutil_test.go:17-19
type closeTracker struct

Methods

Close
Method

Returns

error
func (*closeTracker) Close() error
{
	c.closed = true
	return nil
}

Fields

Name Type Description
closed bool
F
function

TestTestHostHTTPAndResolve

Parameters

app/testing/testutil_test.go:26-69
func TestTestHostHTTPAndResolve(t *testing.T)

{
	host := NewTestHost(func(b *di.Builder) {
		di.RegisterInstance[*testService](b, &testService{Name: "svc"})
	}, func(app *web.Server, container *di.Container) error {
		app.MapGet("/ping", func(ctx *web.Context) error {
			return ctx.JSON(201, map[string]string{"ok": "yes"})
		})
		app.MapPost("/echo", func(ctx *web.Context) error {
			var body map[string]string
			if err := ctx.Bind(&body); err != nil {
				return err
			}
			return ctx.JSON(200, body)
		})
		return nil
	})
	defer host.Close()

	service := Resolve[*testService](host)
	if service.Name != "svc" {
		t.Fatalf("Resolve() name = %q, want svc", service.Name)
	}

	resp := host.Get("/ping")
	if resp.Error != nil {
		t.Fatalf("Get() error = %v", resp.Error)
	}
	if resp.StatusCode != 201 {
		t.Fatalf("Get() status = %d, want 201", resp.StatusCode)
	}

	var decoded map[string]string
	if err := resp.Decode(&decoded); err != nil {
		t.Fatalf("Decode() error = %v", err)
	}
	if decoded["ok"] != "yes" {
		t.Fatalf("Decode()[ok] = %q, want yes", decoded["ok"])
	}

	post := host.Post("/echo", "application/json", strings.NewReader(`{"msg":"hello"}`))
	if post.StatusCode != 200 {
		t.Fatalf("Post() status = %d, want 200", post.StatusCode)
	}
}
F
function

TestTestHostCleanup

Parameters

app/testing/testutil_test.go:71-81
func TestTestHostCleanup(t *testing.T)

{
	host := NewTestHost(nil, nil)
	called := false
	host.Cleanup(func() { called = true })

	host.Close()

	if !called {
		t.Fatal("cleanup was not called")
	}
}
F
function

TestFakeLoggerAssertLogged

Parameters

app/testing/testutil_test.go:83-88
func TestFakeLoggerAssertLogged(t *testing.T)

{
	logger := NewFakeLogger()
	logger.Entries = append(logger.Entries, "ready")

	logger.AssertLogged(t, "ready")
}
F
function

TestTestContext

Parameters

app/testing/testutil_test.go:90-100
func TestTestContext(t *testing.T)

{
	ctx := NewTestContext()
	ctx.Vars["key"] = "value"

	if ctx.Ctx == nil {
		t.Fatal("Ctx is nil")
	}
	if ctx.Vars["key"] != "value" {
		t.Fatalf("Vars[key] = %v, want value", ctx.Vars["key"])
	}
}
F
function

TestNewTestHostClosesContainerWhenRegistrationFails

Parameters

app/testing/testutil_test.go:102-118
func TestNewTestHostClosesContainerWhenRegistrationFails(t *testing.T)

{
	tracker := &closeTracker{}
	defer func() {
		if recover() == nil {
			t.Fatal("NewTestHost() did not panic")
		}
		if !tracker.closed {
			t.Fatal("NewTestHost() leaked the built container")
		}
	}()

	NewTestHost(func(builder *di.Builder) {
		di.RegisterInstance[io.Closer](builder, tracker)
	}, func(*web.Server, *di.Container) error {
		return errors.New("registration failed")
	})
}