Test go
My personal, curated set of artifacts for AI coding agents
npx -y skills add sergeyklay/.agents --skill test-goAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Write, review, and improve Go test code for this project. Use whenever generating, reviewing, or modifying Go tests - including when invoked by the Tester agent, the /test prompt, or any test-related request. Covers table-driven tests, subtests, t.Parallel(), test helpers with t.Helper(), error assertions via errors.As/errors.Is, fixture loading from testdata/, httptest servers, env-gated integration tests, mock/fake/spy patterns, and adapter conformance. Do NOT use for benchmarks or performance profiling.
SKILL.md
13.4 KB, as published. Nobody here has run it
Go Testing
Placeholders
Examples below use two placeholders. They are not literal - substitute the actual names from the project being worked on, matching the case of the surrounding context:
{PROJECT}- the project's identifier (e.g. env-var prefix, repo name). In env vars:MYAPP_…. In prose:MyAppormyapp.{integration}- an external system or adapter name (e.g.jira,stripe,claude). In paths:internal/tracker/jira/. In env vars:JIRA_…. In type names:JiraAdapter.
.go files in assets/ use concrete-looking sample names (Adapter, MYAPP_INTEGRATION_TEST, etc.) so the templates stay syntactically valid Go; comments inside each template tell you what to rename.
Decision Framework
Before writing any test, determine which category it belongs to:
| Category | Characteristics | Run condition |
|---|---|---|
| Unit | Deterministic, no I/O, no network | Always (make test) |
| Unit with fixtures | Reads testdata/ files, uses t.TempDir() | Always |
| Unit with httptest | Spins up httptest.NewServer, tests HTTP adapters | Always |
| Integration | Talks to real external service | Env-gated by per-adapter variable: {PROJECT}_{INTEGRATION}_TEST=1 |
Pick the lightest category that validates the behavior.
Canonical Test Structure
Every test file in this project follows this skeleton. Internalize it - do not deviate.
package pkg // or pkg_test for black-box
import (
"testing"
// stdlib, then project imports, then third-party
)
// --- Test helpers (file-scoped, before test functions) ---
func helperName(t *testing.T, args ...) ReturnType {
t.Helper()
// setup or assertion logic
// use t.Cleanup() for teardown, never defer in helpers
}
// --- Tests ---
func TestFunctionName(t *testing.T) {
t.Parallel()
// ...
}
Key rules this project enforces:
t.Helper()is the first statement in every helper - no exceptions.t.Cleanup()for teardown in helpers;deferonly in test functions themselves.t.Parallel()at both test and subtest level for independent cases.t.TempDir()for filesystem isolation - never write to fixed paths.t.Setenv()for environment variable isolation in tests.- Errors use
errors.As()/errors.Is()- never string comparison.
Table-Driven Tests
Use when multiple cases share identical execution logic. This is the dominant pattern in this project.
func TestSanitizeKey(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
want string
wantErr bool
}{
{"simple key", "ABC-123", "ABC-123", false},
{"empty input", "", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := SanitizeKey(tt.input)
if tt.wantErr {
if err == nil {
t.Fatalf("SanitizeKey(%q) = %q, want error", tt.input, got)
}
return
}
if err != nil {
t.Fatalf("SanitizeKey(%q) unexpected error: %v", tt.input, err)
}
if got != tt.want {
t.Errorf("SanitizeKey(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
When NOT to use tables: cases needing different setup, conditional mocking, or complex branching. Write separate t.Run blocks or separate test functions instead.
Table struct conventions:
- Always include
name stringas the first field - Use
wantErr boolfor error presence; addwantKindfield for typed error checking - Use field names (not positional) when structs have more than 3 fields
- Include inputs in failure messages:
FuncName(%v) = %v, want %v
Error Testing
This project uses custom typed errors extensively. Test error semantics, never strings.
// Domain error types: TrackerError, ConfigError, PathError, TemplateError
// Each has a Kind or Field for categorization
// Pattern: typed error assertion helper
func assertTrackerErrorKind(t *testing.T, err error, want domain.TrackerErrorKind) {
t.Helper()
if err == nil {
t.Fatalf("expected error with kind %q, got nil", want)
}
var te *domain.TrackerError
if !errors.As(err, &te) {
t.Fatalf("error type = %T, want *domain.TrackerError", err)
}
if te.Kind != want {
t.Errorf("TrackerError.Kind = %q, want %q", te.Kind, want)
}
}
Error testing rules:
errors.As()for type assertion - validates the error chain, not just the toperrors.Is()for sentinel comparison- Test the
Kind/Field/Opof typed errors, not.Error()strings t.Fatalwhen nil-error means subsequent assertions will panic;t.Errorotherwise
Test Helpers
Helpers belong at the top of the test file, before test functions. Each adapter package defines its own helpers - do not create a shared testutil package.
Common helper patterns in this project:
// Factory helper - creates a valid test subject or fails.
// Rename Adapter / NewAdapter to the concrete adapter type from your package.
// e.g. {Integration}Adapter / New{Integration}Adapter.
func mustAdapter(t *testing.T, config map[string]any) *Adapter {
t.Helper()
a, err := NewAdapter(config)
if err != nil {
t.Fatalf("NewAdapter: %v", err)
}
return a.(*Adapter)
}
// Fixture loader - reads testdata/ files
func loadFixture(t *testing.T, name string) []byte {
t.Helper()
data, err := os.ReadFile("testdata/" + name)
if err != nil {
t.Fatalf("reading fixture %s: %v", name, err)
}
return data
}
// Config builder - returns valid baseline config for modification
func validConfig(endpoint string) map[string]any {
return map[string]any{
"endpoint": endpoint,
"api_key": "[email protected]:api_token_123",
"project": "PROJ",
}
}
// Resource cleanup helper
func closeStore(t *testing.T, s *Store) {
t.Helper()
if err := s.Close(); err != nil {
t.Errorf("Close: %v", err)
}
}
Naming conventions:
mustX- creates X or fatals (setup that cannot fail gracefully)validX/defaultX- returns baseline config/params for test customizationloadFixture- reads fromtestdata/assertX/requireX- assertion helpers (requirefatals,asserterrors)
HTTP Adapter Testing
Adapter tests use httptest.NewServer with handler functions that return fixture data. Never mock the http.Client itself.
func TestFetchIssues(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request details
if got := r.Header.Get("Authorization"); got == "" {
t.Error("missing Authorization header")
}
// Return fixture response
w.Header().Set("Content-Type", "application/json")
w.Write(loadFixture(t, "search_single_page.json"))
}))
defer srv.Close()
adapter := mustAdapter(t, validConfig(srv.URL))
issues, err := adapter.FetchIssuesByStates(context.Background(), []string{"To Do"})
if err != nil {
t.Fatalf("FetchIssuesByStates: %v", err)
}
// Assert on normalized domain objects, not raw JSON
}
Rules for httptest usage:
- Verify request headers, query params, and method inside the handler
- Return fixture JSON from
testdata/- do not inline large JSON strings - Assert on domain-level objects after adapter normalization, not raw payloads
- Use
atomiccounters when verifying call counts across concurrent requests
Integration Tests (Env-Gated)
Integration tests talk to real external services. They MUST be gated by environment variables and skip cleanly when disabled.
Read references/integration-tests.md for the full integration testing protocol including skip helpers, required env vars, and CI configuration.
Quick reference:
// Replace MYAPP with the project's env-var prefix and INTEGRATION
// with the adapter name (e.g. STRIPE, GITHUB).
func skipUnlessIntegration(t *testing.T) {
t.Helper()
if os.Getenv("MYAPP_INTEGRATION_TEST") != "1" {
t.Skip("skipping integration test: set MYAPP_INTEGRATION_TEST=1 to enable")
}
}
- File naming:
integration_test.go(separate from unit tests) - Skip with
t.Skip, not silent pass - skipped tests are visible in output - Use isolated test data; clean up artifacts when practical
- Never fail CI when env var is absent
Adapter Conformance Testing
Every adapter (tracker or agent) must prove it satisfies the domain interface. Use compile-time interface checks and conformance test suites.
// Compile-time interface satisfaction - place in test file.
// Replace Adapter with the concrete adapter type from your package.
var _ domain.TrackerAdapter = (*Adapter)(nil)
var _ domain.AgentAdapter = (*mockAgentAdapter)(nil)
What conformance tests must cover (per architecture Section 17):
- Normalized field mapping (issue state, priority, labels → domain types)
- Pagination handling (order preserved across pages)
- Error category mapping (transport, auth, API, payload → typed errors)
- Config validation (required fields, defaults, invalid combinations)
Mock and Test Double Patterns
This project uses three kinds of test doubles - pick the lightest one that works.
| Double | Purpose | Example |
|---|---|---|
| Stub | Returns fixed data | validConfig() returning a map |
| Fake | Simplified working implementation | internal/agent/mock package, internal/tracker/file adapter |
| Spy | Records interactions for later assertion | httptest handler with atomic counters |
Registered fakes (when this project provides them, e.g. internal/agent/mock/, internal/tracker/file/) are first-class adapters in the registry. Use them for orchestrator-level tests that need controllable adapter behavior - check the project layout for the actual package paths.
Mock struct pattern:
type mockTrackerAdapter struct{}
var _ domain.TrackerAdapter = (*mockTrackerAdapter)(nil)
func (m *mockTrackerAdapter) FetchIssuesByStates(ctx context.Context, states []string) ([]domain.Issue, error) {
return nil, nil
}
// ... implement all interface methods
Fixture Management
Store test data in testdata/ within the package directory. Go tooling ignores this directory during builds.
internal/tracker/{integration}/testdata/
search_single_page.json
search_multi_page_1.json
search_multi_page_2.json
issue_detail.json
comments.json
Rules:
- One
testdata/directory per package that needs fixtures - Name fixtures descriptively:
search_empty.json,malformed.json,comments_multi_page_1.json - Load via
loadFixture(t, name)helper - never hardcode paths in test functions - JSON fixtures should be real (or realistic) API responses, not minimal stubs
Failure Message Format
Every assertion must produce a message diagnosable without reading the test source.
Format: FuncName(inputs) = got, want expected
// Correct - includes function, input, got, want
t.Errorf("SanitizeKey(%q) = %q, want %q", tt.input, got, tt.want)
t.Errorf("TrackerError.Kind = %q, want %q", te.Kind, want)
// Incorrect - missing context
t.Errorf("got %q, want %q", got, tt.want)
t.Error("wrong result")
- Always
gotbeforewantin message ordering - Use
%qfor strings (shows quotes and escapes),%vfor general values - Use
%dfor integers,%ffor floats - match the type
Validation Checklist
After writing or modifying tests, verify:
-
make testpasses with-race(the default) - New test functions have
t.Parallel()where appropriate - All helpers call
t.Helper()as first statement - Error assertions use
errors.As()/errors.Is(), not string comparison - Failure messages include function name, inputs, got, and want
- Integration tests skip cleanly without their env var
- No external assertion libraries introduced (use stdlib +
cmp.Diffif needed) - Fixtures live in
testdata/and are loaded via helper -
t.TempDir()used for any filesystem operations