How to organize the <pkg>test/ directory that testkit generators write into.
Given a source package crypto/ with interfaces Hasher and Signer:
crypto/
hasher.go # Hasher interface + //go:generate directives
signer.go # Signer interface + //go:generate directives
errors.go # ErrNotFound, ErrInvalid, etc.
sample.go # SampleDigest(h Hasher) Digest — impl-aware sample builders
inmemory.go # production implementations
cryptotest/
# ── Generated (DO NOT EDIT) ──────────────────────────────
hasher_stub.gen.go # testkit stub — test double
hasher_spec.gen.go # testkit suite — Tier 1 conformance
hasher_bench.gen.go # testkit bench — Tier 4 benchmarks
hasher_model.gen.go # testkit model — Tier 2-3 property testing (planned)
signer_stub.gen.go
signer_spec.gen.go
signer_bench.gen.go
# ── Hand-written ─────────────────────────────────────────
hasher_stub.go # stub companion: NewStdlibHasherStub wrapping DelegateTo
signer_stub.go # stub companion for Signer
sample_helpers.go # test-only sample builders (when not in source package)
spec_test.go # TestHasherContract, TestSignerContract, Benchmark*
model_test.go # model wiring: TestHasherModel, FuzzHasherModel
| Pattern | Owner | Purpose |
|---|---|---|
*.gen.go |
Generator | Regenerated by go generate. Never edit. |
*_stub.go |
Developer | Stub companion — wraps DelegateTo with a production impl for integration-style stubs. Optional; only needed when consumers want a pre-wired stub. |
sample_helpers.go |
Developer | Sample builder functions that don't belong in the source package's public API. Referenced by //testkit:sample directives. |
spec_test.go |
Developer | Wires AssertHasherContract(t, factory, ...) and BenchmarkHasherContract(b, factory) with the factory closure and options. One per interface, or combine small interfaces. |
model_test.go |
Developer | Wires HasherModelTest, HasherModelFuzz with factory, reference, extra actions/laws. Separate from spec because model tests are heavier and may need different build tags. (planned — model generator not yet shipped.) |
Sample builders (//testkit:sample SampleDigest SampleDigest) can live in two places:
Source package (recommended default). The generator qualifies the name: crypto.SampleDigest(impl).
// crypto/sample.go
func SampleDigest(h Hasher) Digest {
return h.Hash([]byte{}) // impl-aware: uses SUT to get correct size
}Use this when the builder calls SUT methods to produce impl-aware values — the common case for crypto, codec, and other shape-dependent interfaces.
Output test package. The generator emits the name unqualified: TestSampleDigest(impl).
// crypto/cryptotest/sample_helpers.go
func TestSampleDigest(_ crypto.Hasher) Digest {
var d Digest; d[0] = 0x42; return d
}Use this when the builder is pure test infrastructure that shouldn't be part of the source package's public API. The generator detects which package the name belongs to by checking the source package's scope first.
The generated stub (hasher_stub.gen.go) provides a raw test double. The companion (hasher_stub.go) wraps it with a production implementation for consumers who want a pre-configured reference:
// crypto/cryptotest/hasher_stub.go
package cryptotest
// NewStdlibHasherStub creates a stub backed by a real stdlib implementation.
func NewStdlibHasherStub(tb testing.TB, spec HasherSpec) *HasherStub {
return NewHasherStub(tb, HasherStubDelegateTo(spec.New()))
}This pattern is particularly useful for model testing, where the stub serves as the reference implementation:
cryptotest.HasherModelReference(func() crypto.Hasher {
return cryptotest.NewStdlibHasherStub(nil, sha256Spec)
})Pass nil as tb when creating stubs inside property iterations — this skips cleanup registration which is forbidden in fuzz bodies.
When crypto/ has Hasher, Signer, and MAC, all generators write into cryptotest/. Symbol collisions are avoided by the interface-name prefix on every generated symbol (HasherStub, SignerStub, HasherOption, SignerOption, etc.).
Group the test wiring by interface:
cryptotest/
spec_test.go # TestHasherContract, TestSignerContract, TestMACContract
model_test.go # TestHasherModel, FuzzHasherModel, TestSignerModel
hasher_stub.go # companion for Hasher
signer_stub.go # companion for Signer
mac_stub.go # companion for MAC
Or split per-interface if the files get large:
cryptotest/
hasher_spec_test.go
hasher_model_test.go
signer_spec_test.go
...
Both layouts work. The key constraint is that all *.gen.go files for a given source package land in the same output directory.
When generating for an interface in another module (the -p flag):
//go:generate testkit suite -p go.thesmos.sh/core/crypto -o cryptotest/hasher_spec.gen.go HasherThe generated code imports from the remote module. Sample builders referenced by //testkit:sample must be resolvable from either the remote source package or the local output package.