Skip to content

Latest commit

 

History

History
130 lines (96 loc) · 5.57 KB

File metadata and controls

130 lines (96 loc) · 5.57 KB

Test Package Layout

How to organize the <pkg>test/ directory that testkit generators write into.

Directory structure

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

File roles

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.)

Where sample builders go

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.

Stub companion pattern

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.

Multiple interfaces, one package

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.

Cross-module generation

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 Hasher

The 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.