| id | go.skill.test-authoring |
| kind | skill |
| name | go-test-authoring |
| description | A workflow for authoring Go tests. Starting from the table-driven test scaffold
generated by gotests, it runs the cycle of test design -> adding test cases ->
running -> fixing defects. Use when: (1) invoked via /go-test-authoring,
(2) asked to "write tests" or "add tests" for Go, (3) asked to implement tests
for a Go function or method.
|
| tags | ["go","test","testing"] |
Go Table-Driven Test Authoring Workflow
Strictly preserve the scaffold generated by gotests, and complete the tests through
the cycle of test design -> adding cases -> running -> fixing.
Workflow Overview
Important: Complete the workflow for each small unit under test (a file, a struct,
or a method) before moving on to the next unit. Never advance all units under test at once.
Loop per unit under test:
1. Generate the test function (gotests)
v
2. Test design (design of inputs, expected values, and mocks)
v
3. Add test cases
v
4. Run the tests and check the results
v on failure
5. Root-cause analysis -> report to the developer if it is an implementation defect
v after approval
6. Fix the defect -> back to 4
v on success
-> Next unit under test
Example of how to proceed
When asked to test multiple files:
- Complete the tests for
authorize_error.go through Stage 1-4
- Complete the tests for
authorize_request.go through Stage 1-4
- Continue likewise, completing one file at a time
Stage 1: Generate the Test Function
Generate the scaffold with gotests
gotests -w -all -use_go_cmp <target_file.go>
The generated test is in the same package (package foo).
Rules for modifying generated code
Principle: Everything is forbidden except the explicitly allowed modifications below.
Absolute prohibitions
Changing the struct definition or type of want is absolutely forbidden.
- Use the
want field type generated by gotests as-is.
- Do not define your own
want struct.
- Adding, removing, or changing the type of
want fields is forbidden.
tests := []struct {
name string
want struct {
value string
err error
}
}{}
tests := []struct {
name string
want *SomeType
wantErr bool
}{}
Allowed modifications (everything else is forbidden)
-
Changing args - only when the input data is subject to destructive mutation
- e.g.
io.Writer, io.Reader
-
Changing the assertion section - only the following two patterns are allowed
- Comparison using
go-cmp
- Individual comparison via a public method's return value
- Note: this is a change to the assertion, not a change to the
want struct.
Stage 2: Test Design
Before adding test cases, design the following:
-
Design of inputs (args)
- Normal-path input patterns
- Boundary values
- Inputs that trigger errors
-
Design of expected values (want)
- The expected output for each input
- The expected error for error cases
-
Mock generation when fields contain an interface
- If the struct under test has an interface-typed field, generate a mock.
- Mock generation rule: use
go:generate go tool moq.
Mock generation procedure
When a fields member is an interface, the procedure differs depending on where the
interface is defined.
Pattern 1: Interface in the same package
Add a go:generate directive to the file where the interface is defined:
Output destination: generated into the mock/ directory.
Pattern 2: Interface from an external package
Define a type and a go:generate directive in the implementation file.
File naming convention:
- Type definition file:
${external_package_name}_types.go
- Mock file:
${external_package_name}_types_mock_test.go
Example: oryfosite_types.go -> oryfosite_types_mock_test.go
package foo
import "github.com/example/external"
type ExternalInterface external.SomeInterface
Important:
- moq does not work in test files (
_test.go), so define this in an implementation file.
- Use a type definition, not a type alias (
=), so that moq recognizes it as an interface.
Output destination: generated as mock_<name>_test.go in the same directory as the
target under test (same package).
Common procedure
- Run
go generate to generate the mock
go generate ./path/to/package/...
- The generated mock is in the same package, so it can be used directly without importing.
Absolute rules for mock generation
- Always generate via the
go generate command.
- Running
go tool moq directly is forbidden.
- Manually editing the generated mock file is forbidden.
Design perspectives
- What behavior to verify
- What the edge cases are
- Whether error handling is appropriate
Stage 3: Add Test Cases
Absolute rules
- Only the following test helper function is allowed
func must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
- Do not share test data - define it independently for each test case.
Procedure for adding test cases
- Based on the Stage 2 design, decide the test case name.
- Define
args and want for each case.
- Add mock setup as needed.
Stage 4: Run the Tests and Check the Results
go test -v ./path/to/package -run TestFunctionName
Analysis procedure on failure
- Check the test structure - whether the test code itself has a problem.
- Check the test data - whether the inputs and expected values are correct.
- Check the implementation - if the above are fine, treat it as an implementation defect.
Stage 5: Defect Reporting
When the test structure and test data have no problems, report to the developer:
- The name of the failing test case
- The input
- The expected value
- The actual result
- The presumed cause (if any)
Stage 6: Defect Fixing
After the developer's approval:
- Fix the implementation code.
- Go back to Stage 4 and re-run the tests.
- Repeat until all tests pass.
Concrete Examples of Assertion Changes
Pattern 1: Compare with go-cmp
import "github.com/google/go-cmp/cmp"
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
Pattern 2: Compare via a public method's return value
got := SomeFunction(tt.args.input)
if got.GetValue() != tt.want {
t.Errorf("GetValue() = %v, want %v", got.GetValue(), tt.want)
}
Mandatory Assertion Requirements
Forbidden patterns
The following assertions are meaningless and forbidden:
-
nil comparison only
if (tt.want == nil) != (got == nil) {
t.Errorf(...)
}
-
Assertions that do not use the want field
if got == nil {
t.Errorf("got nil")
}
-
Skipping verification with conditionals
if tt.args.key == "sub" {
}
Mandatory patterns
Follow these in every test:
-
Value comparison with go-cmp is mandatory
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
-
Use cmp.AllowUnexported for types that contain private fields
if diff := cmp.Diff(tt.want, got, cmp.AllowUnexported(SomeType{})); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
-
Define a concrete want value for every test case
- If there is a
want field, it must be used.
- A "do not verify" case must not exist.
Principles of Test Case Design
Designing want values
-
Define a concrete want value for every test case
- Make the expected value explicit, including nil, empty, and error.
-
Separate by test case rather than by conditional
if tt.args.isError {
} else {
}
{
name: "normal case",
want: expectedValue,
},
{
name: "error case",
wantErr: true,
},
Self-Check Before Implementing Tests
Confirm the following before running the tests: