Skip to content

Adding Check Functions

go-testgen generates checkXxxError automatically. All other assertions are written by you. This page explains how to write good check functions and where to put them.

Anatomy of a Check Function

A check function is a factory that returns a checkXxxFn closure:

// Factory — takes assertion parameters
func checkUserName(want string) checkServiceCreateUserFn {
    // Closure — called by the test runner after the act
    return func(t *testing.T, u *userDomain.User, err error) {
        t.Helper()                        // failures point to the call site
        if err != nil || u == nil {
            return                        // skip assertion if precondition failed
        }
        assert.Equalf(t, want, u.Name,
            "checkUserName: got %q, want %q", u.Name, want)
    }
}

Rules to follow:

  1. Always call t.Helper() as the first line inside the closure.
  2. Guard against nil before dereferencing — a previous check may have already failed.
  3. Use assert.Xxxf (not assert.Xxx) — the format argument makes failures self-describing.
  4. Name the function after what it asserts: checkUserName, checkOrderTotal, checkResponseCode.

Parameterless vs. Parameterized

Some checks do not need parameters:

func checkUserIDNotEmpty() checkServiceCreateUserFn {
    return func(t *testing.T, u *userDomain.User, _ error) {
        t.Helper()
        assert.NotEmptyf(t, u.ID, "checkUserIDNotEmpty: ID should not be empty after creation")
    }
}

Parameterized checks are closures that capture the expected value:

func checkUserEmail(want string) checkServiceCreateUserFn {
    return func(t *testing.T, u *userDomain.User, _ error) {
        t.Helper()
        assert.Equalf(t, want, u.Email, "checkUserEmail mismatch")
    }
}

Where to Put Check Functions

In the same test file (most common)

For checks specific to one function, define them near the TestXxx function that uses them:

// service_test.go

type checkServiceCreateUserFn func(*testing.T, *userDomain.User, error)
var checkServiceCreateUser = ...

func checkUserName(want string) checkServiceCreateUserFn { ... }
func checkUserIDNotEmpty() checkServiceCreateUserFn { ... }

func TestService_CreateUser(t *testing.T) { ... }

In a shared testutil package (reuse across files)

When multiple test files in a package need the same checks, extract them:

// internal/core/services/user/testutil_test.go
package user_test

func checkUserName(want string) checkServiceCreateUserFn { ... }
func checkUserIDNotEmpty() checkServiceCreateUserFn { ... }

Or a dedicated testutil package for cross-package reuse:

// pkg/testutil/user_checks.go
package testutil

// Exported check factories (note: signature must match your check type)
func CheckUserName(want string) func(*testing.T, *userDomain.User, error) { ... }

This method increases the binary size as it doesn't ends with a _test.go suffix, therefore it won't be ignored at compile time

Composing Checks

In the test table, compose checks using the collector:

checks: checkServiceCreateUser(
    checkServiceCreateUserError(""),   // no error
    checkUserName("alice"),
    checkUserIDNotEmpty(),
    checkUserEmail("alice@example.com"),
),

Order matters only if a check guards against nil for subsequent ones. Otherwise, checks are independent — a failure in one does not skip the others (because assert does not stop the test, only require does).

Upgrading from wantErr bool to Check Functions

If you have existing tests with wantErr bool, you can migrate incrementally. Add a checks field alongside the existing fields and move assertions one by one:

// Before
{name: "valid", input: "alice", want: &User{Name: "alice"}, wantErr: false}

// After
{
    name:  "valid",
    input: "alice",
    checks: checkCreateUser(
        checkCreateUserError(""),
        checkUserName("alice"),
    ),
}

Example: Full Check Suite for a Method

// --- check type and collector (generated by go-testgen) ---

type checkServiceCreateUserFn func(*testing.T, *userDomain.User, error)

var checkServiceCreateUser = func(fns ...checkServiceCreateUserFn) []checkServiceCreateUserFn {
    return fns
}

func checkServiceCreateUserError(want string) checkServiceCreateUserFn {
    return func(t *testing.T, _ *userDomain.User, err error) {
        t.Helper()
        if want == "" {
            assert.NoErrorf(t, err, "unexpected error: %v", err)
            return
        }
        if assert.Errorf(t, err, "expected error %q", want) {
            assert.Containsf(t, err.Error(), want, "error mismatch")
        }
    }
}

// --- domain-specific checks (written by hand) ---

func checkUserName(want string) checkServiceCreateUserFn {
    return func(t *testing.T, u *userDomain.User, _ error) {
        t.Helper()
        if u == nil {
            return
        }
        assert.Equalf(t, want, u.Name, "checkUserName mismatch")
    }
}

func checkUserIDNotEmpty() checkServiceCreateUserFn {
    return func(t *testing.T, u *userDomain.User, _ error) {
        t.Helper()
        if u == nil {
            return
        }
        assert.NotEmptyf(t, u.ID, "checkUserIDNotEmpty: ID is empty")
    }
}

func checkUserEmail(want string) checkServiceCreateUserFn {
    return func(t *testing.T, u *userDomain.User, _ error) {
        t.Helper()
        if u == nil {
            return
        }
        assert.Equalf(t, want, u.Email, "checkUserEmail mismatch")
    }
}