Documentation
¶
Index ¶
- func Do[V Vocabulary, F Body](v V, fn F) V
- func DoAs[V Vocabulary, F Body](v V, name string, fn F) V
- func DoFor[K any, V Vocabulary, F Body](v V, fn F) V
- func NameOf[K any]() string
- func Resolve[T any](r Resolver) T
- func TryResolve[T any](r Resolver) (T, bool)
- type Artifact
- type Body
- type Chain
- func (c *Chain) Add(name string, fn StepFunc) *Chain
- func (c *Chain) All(steps ...Step) *Chain
- func (c *Chain) And(steps ...Step) *Chain
- func (c *Chain) Attempt(fn func(Host) error) error
- func (c *Chain) AttemptAs(name string, fn func(Host) error) error
- func (c *Chain) AttemptFor[K any](fn func(Host) error) error
- func (c *Chain) Context() context.Context
- func (c *Chain) Get[T any](fn func(Host) (T, error)) T
- func (c *Chain) GetAs[T any](name string, fn func(Host) (T, error)) T
- func (c *Chain) GetFor[K, T any](fn func(Host) (T, error)) T
- func (c *Chain) TB() TB
- func (c *Chain) Try[T any](fn func(Host) (T, error)) Outcome[T]
- func (c *Chain) TryAs[T any](name string, fn func(Host) (T, error)) Outcome[T]
- func (c *Chain) TryFor[K, T any](fn func(Host) (T, error)) Outcome[T]
- func (c *Chain) WithContext(ctx context.Context) *Chain
- type Container
- type ContainerBuilder
- type Executor
- type FailMode
- type Fixture
- type Host
- type Observer
- type Outcome
- type PanicError
- type PathResolver
- type Phase
- type ResolveError
- type Resolver
- type Scope
- type Setup
- func (s *Setup) Enter[A, C, I Phase](t TB) *Fixture[A, C, I]
- func (s *Setup) EnterContext[A, C, I Phase](ctx context.Context, t TB) *Fixture[A, C, I]
- func (s *Setup) EnterStage(t TB) *Stage
- func (s *Setup) EnterStageContext(ctx context.Context, t TB) *Stage
- func (s *Setup) Observe(observers ...Observer) *Setup
- type Stage
- func (s *Stage) Act() *Chain
- func (s *Stage) Arrange() *Chain
- func (s *Stage) Chain(phase string, mode FailMode) *Chain
- func (s *Stage) Close() error
- func (s *Stage) Context() context.Context
- func (s *Stage) Host() Host
- func (s *Stage) ID() string
- func (s *Stage) Inspect() *Chain
- func (s *Stage) TB() TB
- func (s *Stage) Tokens() *Tokens
- func (s *Stage) TryResolveType(typ reflect.Type) (any, bool)
- func (s *Stage) TryResolveTypePath(typ reflect.Type, path []reflect.Type) (any, bool)
- type StageContext
- type Step
- type StepEvent
- type StepFunc
- type TB
- type Token
- type Tokens
- type Vocabulary
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Do ¶ added in v0.3.0
func Do[V Vocabulary, F Body](v V, fn F) V
Do runs fn as a step of v's chain and hands v back, so a verb is one return:
func (a Arrange) SequenceYields(ids ...int64) Arrange {
a.Helper()
return mokkit.Do(a, func(h mokkit.Host) {
h.Resolve[*fakeSequence]().values = ids
})
}
The step is named after the verb that called Do; a Step keeps its own name.
func DoAs ¶ added in v0.3.0
func DoAs[V Vocabulary, F Body](v V, name string, fn F) V
DoAs is Do with the step's name given, for a verb whose own name is not the one to report under.
func DoFor ¶ added in v0.3.0
func DoFor[K any, V Vocabulary, F Body](v V, fn F) V
DoFor is Do for a verb generic over a role: the role K is appended to the step's name in brackets, as UserExists[Buyer].
func NameOf ¶
NameOf reports a token's bare name, so a verb can build a readable step label or seed an identifier with the role it is acting for.
func Resolve ¶
Resolve returns the service registered as T.
It panics when nothing is registered; inside a Step, the executor recovers the panic and reports it as a failure attributed to the step, so vocabulary calls Resolve without error handling. Inside a verb, prefer Host.Resolve.
func TryResolve ¶
TryResolve returns the service registered as T, reporting false when nothing is registered under that type or the registration cannot be used as T.
Types ¶
type Artifact ¶
type Artifact[T any] struct{}
Artifact is the phantom a token embeds to declare what it names:
type Buyer struct{ mokkit.Artifact[User] }
One line declares both the role and the type of the thing that role stands for; the type is inferred at every call site.
type Body ¶ added in v0.3.0
A Body is the work a verb hands to Do. It receives the stage's Host and reports failure by returning an error; a body that cannot fail returns nothing. A Step is a body too, so a verb can run vocabulary from another package and keep its own type.
type Chain ¶
type Chain struct {
// Tokens is embedded so a verb reaches its artifacts the same way a test
// does — a.New[Buyer]() to produce, a.Of[Buyer]() to read.
*Tokens
// contains filtered or unexported fields
}
A Chain is one phase of a test — arrange, act or inspect — that executes each step as it is added. There is no terminal call: by the time a verb returns, its step has run.
Vocabulary is authored by embedding a *Chain in your own type and hanging verbs on it:
type Arrange struct{ *mokkit.Chain }
func (a Arrange) UserExists[K mokkit.Token[User]](s Status) Arrange {
a.Helper()
a.Add("UserExists["+mokkit.NameOf[K]()+"]", func(ctx context.Context, h mokkit.Host) error {
*a.New[K]() = newUser(s)
h.Resolve[*MockUsers]().EXPECT()...
return nil
})
return a
}
func (*Chain) Add ¶
Add runs one named step and reports failure according to the chain's FailMode. The name is what identifies the step in a failure message, so it should be the verb the reader wrote — including the role it acted for, which NameOf supplies.
func (*Chain) All ¶
All runs steps concurrently and continues once every one has finished, so the group is unordered within itself but ordered against its neighbors in the chain.
Under FailFast the first failure reported ends the test, so later failures in the same group are not shown; under FailSoft every failure is reported. Branches run on their own goroutines and must report by returning an error, never by calling the test's own Fatal.
func (*Chain) And ¶
And runs vocabulary authored as plain functions, including from packages that cannot add methods to this chain's type, with the chain unbroken:
f.Arrange().
UserExists[Buyer](Vip).
And(cachevocab.HasUser[Buyer](f)).
RateIs(Vip, 0.15)
A vocabulary type re-declares this to keep its own return type, and may name it whatever reads best there — And, Also, Then.
func (*Chain) Attempt ¶ added in v0.3.0
Attempt runs fn as a step and returns the error it reported without failing the chain on it: Try for an operation whose only outcome is whether it failed. A panic inside fn still fails the chain.
The step is named after the verb that called Attempt.
func (*Chain) AttemptFor ¶ added in v0.3.0
AttemptFor is Attempt for a verb generic over a role, named as DoFor names its step.
func (*Chain) Get ¶ added in v0.3.0
Get runs fn as a step and returns what it produced. An error fails the chain according to its FailMode; the value is returned only when there was none.
func (a Act) Discount(userID string) Result {
a.Helper()
return a.Get(func(h mokkit.Host) (Result, error) {
return h.Resolve[*Service]().Calculate(h.Context(), userID)
})
}
The step is named after the verb that called Get.
func (*Chain) GetFor ¶ added in v0.3.0
GetFor is Get for a verb generic over a role, named as DoFor names its step.
func (*Chain) TB ¶
TB reports the test this chain belongs to. Hand it, not the chain, to an assertion library: assert suits Inspect's soft failure, require suits Arrange's hard one.
func (*Chain) Try ¶ added in v0.3.0
Try runs fn as a step and returns its outcome without failing the chain on the error, so a test for a refusal inspects the error the way it inspects a value. A panic inside fn still fails the chain.
The step is named after the verb that called Try.
func (*Chain) TryFor ¶ added in v0.3.0
TryFor is Try for a verb generic over a role, named as DoFor names its step.
func (*Chain) WithContext ¶
WithContext runs subsequent steps with ctx. It mutates the chain and returns it, exactly as And and All do, so a vocabulary type's forwarder is written the same way as theirs:
func (a Arrange) WithContext(ctx context.Context) Arrange {
a.Helper(); a.Chain.WithContext(ctx); return a
}
It does not affect steps already run.
type Container ¶
type Container interface {
BeginScope(ctx context.Context, sc StageContext) (Scope, error)
}
A Container is a built, immutable composition. It hands out one Scope per entered stage.
type ContainerBuilder ¶
A ContainerBuilder composes one container. Builders are run once, by NewSetup, before any stage is entered.
The C# original ran builders through four phases so a DI builder could see a mock builder's registrations and bridge them. Hand-wiring removes the need: a factory takes the stage Resolver and pulls collaborators itself. If a runtime-DI adapter ever needs peer visibility, it arrives as an optional interface NewSetup type-asserts for, which is additive.
type Executor ¶
type Executor interface {
// Run executes one step. The Step arrives whole — name included — so an
// executor that traces or reports has something to say about it.
Run(ctx context.Context, s Step) error
Close() error
}
An Executor runs steps against a stage.
The chain API is identical whichever Executor is in use, so a channel-backed executor — one worker goroutine per stage, allowing steps to be posted from other goroutines and giving tracing a single choke point — can replace the inline one without touching Chain or any vocabulary.
type FailMode ¶
type FailMode int
FailMode decides what a failing step does to the test.
const ( // FailFast reports through t.Fatal, which unwinds the test goroutine — so // the rest of the chain never runs. Used by Arrange and Act, where a broken // setup makes every later step meaningless. FailFast FailMode = iota // FailSoft reports through t.Error and carries on, so a run reports every // failing observation rather than only the first. Used by Inspect. FailSoft )
type Fixture ¶ added in v0.3.0
type Fixture[A, C, I Phase] struct { *Tokens // Stage is the runtime the fixture's chains run against. Stage *Stage }
A Fixture is what a test body talks to: the three phases, typed as the suite's own vocabulary, and the artifacts its verbs produce. Embedding *Tokens puts f.New, f.Of and f.Ref on the fixture itself.
type fixture = mokkit.Fixture[Arrange, Act, Inspect]
func newFixture(t *testing.T) *fixture {
t.Helper()
b := bag.New()
bag.Fresh[fakeUsers](b)
bag.Scoped(b, func(r mokkit.Resolver) *Service { ... })
return mokkit.Enter[Arrange, Act, Inspect](t, b)
}
func Enter ¶ added in v0.3.0
func Enter[A, C, I Phase](t TB, builders ...ContainerBuilder) *Fixture[A, C, I]
Enter builds the containers, opens a stage for t and returns the fixture over it. It is for a composition built per test; a composition shared by a package is built once with NewSetup and entered with Setup.Enter. A container that cannot be built fails t.
func EnterContext ¶ added in v0.3.0
func EnterContext[A, C, I Phase](ctx context.Context, t TB, builders ...ContainerBuilder) *Fixture[A, C, I]
EnterContext is Enter with an explicit context, which the containers are built with and the stage's steps run with.
func (*Fixture[A, C, I]) Act ¶ added in v0.3.0
func (f *Fixture[A, C, I]) Act() C
Act starts a chain for the operation under test, typed as the suite's Act.
type Host ¶
type Host struct {
// contains filtered or unexported fields
}
Host is what a Step receives: the stage's resolver plus its context. It is a struct rather than an interface so that Resolve can be a generic method on it.
func (Host) Resolve ¶
Resolve returns the service registered as T, panicking when nothing is. Inside a Step that is intentional: the executor recovers the panic and reports it against the step, so vocabulary resolves without error handling.
func (Host) Resolver ¶
Resolver reports the underlying resolver, for the places that still want the interface — passing it to a container, or to the free Resolve.
func (Host) TryResolve ¶
TryResolve returns the service registered as T, reporting false when nothing is registered under that type.
type Observer ¶
type Observer interface {
// StageEntered reports a test beginning to run against the composition.
StageEntered(test, stageID string)
// StepRan reports one step, after it finished.
StepRan(event StepEvent)
// StageClosed reports the stage releasing its scopes. failed is the test's
// verdict at that moment — soft failures included, since cleanup runs after
// the test body.
StageClosed(test, stageID string, failed bool)
}
An Observer hears what a composition's stages do: a stage entered per test, every step that ran, and the stage closing with the test's verdict. It is the seam a reporter plugs into — the steps arrive already carrying the vocabulary names the suite was written in, so a report reads as the scenario did.
Implementations must be safe for concurrent use: All runs its branches on their own goroutines, and suites may run tests in parallel. Events for one stage may therefore interleave with another's; StageID is what groups them.
An Observer must not fail the test.
type Outcome ¶ added in v0.3.0
An Outcome is what Try hands back: the value fn produced and the error it returned. Exactly one of them is meaningful.
type PanicError ¶
PanicError reports a panic raised inside a step, carrying the stack from the point of the panic rather than from where it was recovered.
func (*PanicError) Error ¶
func (e *PanicError) Error() string
func (*PanicError) Unwrap ¶
func (e *PanicError) Unwrap() error
Unwrap lets errors.As reach a panicked error value, so a caller can test for *ResolveError after Resolve panicked inside a step.
type PathResolver ¶
type PathResolver interface {
Resolver
// TryResolveTypePath resolves t with path naming the types already under
// construction on this goroutine, outermost first.
TryResolveTypePath(t reflect.Type, path []reflect.Type) (any, bool)
}
PathResolver is the optional half of the contract: a resolver that can carry the chain of types currently under construction. A container detects a dependency cycle by checking that chain, and the chain has to survive the hop through the stage — otherwise a cycle spanning two containers is invisible to both and deadlocks on a lock one of them already holds.
Implement it on a Scope whose factories can resolve their own collaborators. Stage implements it, and threads the path through every scope that does.
type Phase ¶ added in v0.3.0
type Phase interface{ ~struct{ *Chain } }
A Phase is a vocabulary type declared as a struct whose only field is an embedded *Chain:
type (
Arrange struct{ *mokkit.Chain }
Act struct{ *mokkit.Chain }
Inspect struct{ *mokkit.Chain }
)
type ResolveError ¶
type ResolveError struct {
Type reflect.Type
// Present reports that something was registered under Type but could not be
// used as it — a container handed back the wrong thing, which is a
// different bug from having registered nothing.
Present bool
}
ResolveError reports a service that no container had registered, or one registered under a type it cannot be used as.
func (*ResolveError) Error ¶
func (e *ResolveError) Error() string
type Resolver ¶
type Resolver interface {
// TryResolveType looks up a service by type. It reports false when nothing
// is registered under t. Implementations must be safe for concurrent use.
TryResolveType(t reflect.Type) (any, bool)
}
Resolver hands back a service registered in a container, keyed by its type. Stage, Scope and a container's own factory resolver all satisfy it, so Resolve and TryResolve work against any of them.
type Scope ¶
A Scope holds the per-stage instances of a container's services. Scopes are resolved from concurrently, so implementations must be safe for concurrent use, and are closed when the stage is.
type Setup ¶
type Setup struct {
// contains filtered or unexported fields
}
A Setup is a composition of containers, built once. Building is the expensive part; entering a stage from it is cheap and happens per test.
func NewSetup ¶
func NewSetup(ctx context.Context, builders ...ContainerBuilder) (*Setup, error)
NewSetup builds every container. Do this once per system-under-test, in whatever "run once" hook the test runner offers — TestMain, not init.
func (*Setup) EnterContext ¶ added in v0.3.0
EnterContext is Enter with an explicit context, which the stage's steps run with.
func (*Setup) EnterStage ¶
EnterStage opens a fresh, isolated stage for one test and registers its cleanup with t. Scoped services are created per stage and released when the test ends, so nothing leaks between tests.
func (*Setup) EnterStageContext ¶
EnterStageContext is EnterStage with an explicit context, which steps in this stage's chains are run with.
func (*Setup) Observe ¶
Observe registers observers for every stage later entered from this setup. Call it once, where the composition is built:
setup, err := mokkit.NewSetup(ctx, mocks, app) ... setup.Observe(allure.New(resultsDir))
It returns the setup, so it chains. Observing after stages have been entered affects only stages entered afterwards.
type Stage ¶
type Stage struct {
// contains filtered or unexported fields
}
A Stage is the runtime one test runs against: the services it resolves, the tokens its artifacts live under, and the executor its steps run on.
func (*Stage) Arrange ¶
Arrange starts a setup chain. It fails fast: a broken setup makes every later step meaningless.
func (*Stage) Chain ¶
Chain starts a phase that fails according to mode. Arrange, Act and Inspect are the named phases; this is for anything else.
func (*Stage) Close ¶
Close releases every scope, newest first, and empties the resolution cache. EnterStage registers it with t.Cleanup, so tests do not normally call it. Closing twice is a no-op.
func (*Stage) Host ¶
Host reports what a step receives, for the occasions that want one outside a chain.
func (*Stage) Inspect ¶
Inspect starts an observation chain. It fails soft, so one run reports every failing observation rather than only the first.
func (*Stage) TB ¶
TB reports the test this stage belongs to, so a fixture accessor can mark itself a helper and keep failures pointing at the test's own line.
func (*Stage) Tokens ¶
Tokens reports the per-test artifact registry backing New and Of. Embed it in a fixture to have f.New[Buyer]() and f.Of[Buyer]() promoted onto it.
func (*Stage) TryResolveType ¶
TryResolveType finds the first container that has typ registered, caching the result so every step and every branch of an All group sees one instance.
func (*Stage) TryResolveTypePath ¶
TryResolveTypePath is TryResolveType carrying the types already under construction, so a container can report a cycle that crosses into another container instead of deadlocking inside it.
The cache lock is not held while a scope resolves, because a container's factory may resolve its own collaborators back through this method; holding it would deadlock. A scope that builds lazily is therefore responsible for handing back the same instance under concurrent resolution — see container/bag.
type StageContext ¶
type StageContext struct {
// T is the test the stage belongs to. A container may use it to bind
// per-test machinery — a gomock Controller, for instance, which asserts its
// expectations on cleanup.
T TB
// StageID is unique per entered stage.
StageID string
// Resolver is the stage itself, spanning every container in the
// composition. A container whose factories build real services uses it to
// pull collaborators another container registered — which is all the
// mock-to-DI bridge amounts to here.
//
// It must only be used lazily, from a factory at resolve time: while
// BeginScope runs, the sibling scopes it would reach are still opening.
Resolver Resolver
}
StageContext describes the stage a scope is being opened for.
type Step ¶
A Step is a StepFunc with the name it reports under when it fails. It is the contract for vocabulary authored as plain functions — including in packages that cannot add methods to a chain's type — which Chain.And then runs:
func HasClient(c *Client) mokkit.Step {
return mokkit.NewStep("cache.HasClient", func(ctx context.Context, h mokkit.Host) error {
...
})
}
The name is given rather than recovered from the runtime because the compiler may inline the verb that built the closure, which would attribute the step to whatever function called it.
type StepEvent ¶
type StepEvent struct {
Test string
StageID string
Phase string
Step string
Started time.Time
Duration time.Duration
Err error
}
A StepEvent describes one step that ran: which test asked for it, the phase and verb the reader wrote, how long it took, and how it ended. Err is nil for a step that passed, the step's own error for one that failed, and a *PanicError for one that crashed — a reporter that wants to distinguish "failed" from "broken" branches on that.
type StepFunc ¶
A StepFunc is the work one unit of a test does: set up a collaborator, run the operation under test, observe an outcome. It receives the stage's Host and reports failure by returning an error.
type TB ¶
type TB interface {
Helper()
Name() string
Cleanup(func())
Errorf(format string, args ...any)
Fatalf(format string, args ...any)
FailNow()
Failed() bool
}
TB is the part of *testing.T that mokkit reports through. *testing.T and *testing.B satisfy it.
Unlike testing.TB it can be implemented — by a fake reporter, or by a runner that is not the standard one — and it is wide enough for the common assertion libraries, so it can be handed straight to assert.Equal or require.NoError.
type Token ¶
type Token[T any] interface { // contains filtered or unexported methods }
Token constrains a role to one artifact type. A verb generic over Token[User] will not accept a token that names an Order, so the pairing is checked by the compiler rather than discovered at run time.
type Tokens ¶
type Tokens struct {
// contains filtered or unexported fields
}
Tokens holds the artifacts a test's verbs produce, keyed by the token that names each one. New hands a producing verb its sink and Of reads the value back, both spelled only with the token, in any phase.
Tokens belongs to a single test. Stage creates one per stage, so nothing leaks between tests.
func (*Tokens) Declared ¶
Declared reports whether the role K has been produced, without producing it and without failing.
func (*Tokens) New ¶
New returns the sink for the role K, creating it on first use. It is the write side: a producing verb fills the pointer it returns.
func (a Arrange) UserExists[K mokkit.Token[User]](s Status) Arrange {
a.Add("UserExists["+mokkit.NameOf[K]()+"]", func(...) error {
*a.New[K]() = newUser(s)
...
})
return a
}
func (*Tokens) Of ¶
Of returns the value filed under the role K, failing the test when no verb has produced it. It is safe to call mid-chain, as an argument to a later verb in the same expression.
func (*Tokens) Ref ¶
Ref returns the artifact filed under the role K as a pointer, failing the test when no verb has produced it.
Use Ref for an artifact with identity — a recording double or probe whose state the Act mutates and a later phase observes. Everywhere else use Of, which hands back a value.
type Vocabulary ¶ added in v0.3.0
type Vocabulary interface {
// contains filtered or unexported methods
}
A Vocabulary is any type that embeds *Chain: a phase such as Arrange, or a scope that carries a value alongside the chain.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
container
|
|
|
bag
Package bag is mokkit's hand-wired container.
|
Package bag is mokkit's hand-wired container. |
|
mokkitgomock
module
|
|
|
report
|
|
|
allure
Package allure is a mokkit.Observer that writes Allure 2 result files, so any Allure consumer — TestOps, the allure CLI — renders a mokkit suite with each test reading as its scenario.
|
Package allure is a mokkit.Observer that writes Allure 2 result files, so any Allure consumer — TestOps, the allure CLI — renders a mokkit suite with each test reading as its scenario. |