Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default defineConfig({
integrations: [
starlight({
title: 'Mokkit',
description: "Write tests that read like a story in your domain's language — as plain, compilable C#.",
description: "Write tests that read like a story in your domain's language — as plain, compilable C# or Go.",
logo: { src: './src/assets/logo.svg', alt: 'Mokkit' },
favicon: '/favicon.svg',
head: [
Expand All @@ -21,7 +21,8 @@ export default defineConfig({
{ tag: 'meta', attrs: { name: 'twitter:card', content: 'summary_large_image' } },
],
social: [
{ icon: 'github', label: 'GitHub', href: 'https://github.com/GrafGenerator/Mokkit' },
{ icon: 'github', label: 'Mokkit for .NET', href: 'https://github.com/GrafGenerator/Mokkit' },
{ icon: 'github', label: 'Mokkit for Go', href: 'https://github.com/GrafGenerator/go-mokkit' },
],
editLink: {
baseUrl: 'https://github.com/GrafGenerator/Mokkit/edit/main/docs/',
Expand All @@ -43,7 +44,8 @@ export default defineConfig({
{ label: 'Building your test vocabulary', slug: 'concepts/vocabulary' },
{ label: 'Scenario tests', slug: 'concepts/scenarios' },
{ label: 'The Stage & lifecycle', slug: 'concepts/stage' },
{ label: 'Captures: Capture vs Trapture', slug: 'concepts/captures' },
{ label: 'Captures: Capture vs Trapture', slug: 'concepts/captures', badge: { text: 'C#', variant: 'note' } },
{ label: 'Tokens: roles as types', slug: 'concepts/tokens', badge: { text: 'Go', variant: 'tip' } },
{ label: 'Containers & the mock→DI bridge', slug: 'concepts/containers' },
],
},
Expand All @@ -59,16 +61,17 @@ export default defineConfig({
{ label: 'Test a Kafka consumer / producer', slug: 'guides/kafka' },
{ label: 'Async / eventually-consistent assertions', slug: 'guides/eventually-consistent' },
{ label: 'Deterministic time & ids', slug: 'guides/deterministic-time-ids' },
{ label: 'Report to Allure', slug: 'guides/reporting-allure', badge: { text: 'Go', variant: 'tip' } },
],
},
{
label: 'Techniques',
items: [
{ label: 'Value & context scopes', slug: 'guides/inspect-scopes' },
{ label: 'Parallel inspects with ThenAll', slug: 'guides/thenall' },
{ label: 'Ensure: derive, guard, capture', slug: 'guides/ensure' },
{ label: 'Snapshot assertions with Verify', slug: 'guides/verify-snapshots' },
{ label: 'Source-generated arranges', slug: 'guides/mokkit-capture' },
{ label: 'Ensure: derive, guard, capture', slug: 'guides/ensure', badge: { text: 'C#', variant: 'note' } },
{ label: 'Snapshot assertions with Verify', slug: 'guides/verify-snapshots', badge: { text: 'C#', variant: 'note' } },
{ label: 'Source-generated arranges', slug: 'guides/mokkit-capture', badge: { text: 'C#', variant: 'note' } },
],
},
{
Expand All @@ -84,7 +87,8 @@ export default defineConfig({
{ label: 'How to structure a test project', slug: 'reference/project-structure' },
{ label: 'Conventions cheat-sheet', slug: 'reference/conventions' },
// DocFX-generated static site under /api (built separately in CI).
{ label: 'API reference', link: '/api/' },
{ label: 'API reference (C#)', link: '/api/' },
{ label: 'API reference (Go)', link: 'https://pkg.go.dev/github.com/GrafGenerator/go-mokkit', attrs: { target: '_blank' } },
],
},
],
Expand Down
87 changes: 0 additions & 87 deletions docs/src/content/docs/concepts/aai.md

This file was deleted.

185 changes: 185 additions & 0 deletions docs/src/content/docs/concepts/aai.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
---
title: Arrange / Act / Inspect
description: The three-phase shape every Mokkit test follows, and the discipline that keeps tests honest.
---

import { Tabs, TabItem } from '@astrojs/starlight/components';

Every Mokkit test tells the same three-part story. It's the classic Arrange-Act-Assert, with the assert
phase renamed **Inspect** to make its one rule explicit: it only *observes*.

## One difference to hold on to: deferred vs eager

The two languages run their chains differently, and most other differences follow from this one:

- **C# chains are deferred.** `.Then(...)` records a step; nothing happens until you `await` the chain.
An arrange hands back a *capture* — a placeholder for a value that doesn't exist yet.
- **Go chains are eager.** By the time a verb returns, its step has already run. There is no terminal
call, no placeholder to hold, and an Act verb can simply *return* its result. Artifacts travel through
[tokens](/concepts/tokens/) instead of captures.

Same story, different engine. The rest of this page shows both.

## Arrange

Arrange sets up the world: stub a collaborator, seed a database, put a message on a queue. Each step
receives the test host, so it can resolve and configure services.

<Tabs syncKey="lang">
<TabItem label="C#">

`stage.Arrange()` starts a fluent chain. Each `.Then(...)` registers a step; **`await`-ing the chain runs
the steps in order:**

```csharp
await stage.Arrange()
.Then(host => host.Execute<Mock<IClock>>(clock =>
clock.Setup(x => x.UtcNow).Returns(FixedNow)))
.Then(async host => await host.ExecuteAsync<Db>(db => db.Seed(...)));
```

Deferral is what lets an arrange hand back a **capture** — a placeholder for a value that doesn't exist
yet:

```csharp
var init = Capture.Start(out Capture<Client> client); // client is empty for now
return arrange.Then(_ => init.Set(new Client(...))); // filled when the chain runs
```

After the `await`, `client.Value` holds the created client. Captures are how the artifacts an arrange
creates flow into later steps. See [Capture vs Trapture](/concepts/captures/).

</TabItem>
<TabItem label="Go">

`stage.Arrange()` starts a chain that runs each step **as it is added** — there is nothing to await:

```go
f.Arrange().
ClockIsFixed(fixedNow).
DbSeeded(rows)
```

An arrange that produces something files it under a **token**, and any later phase reads it back by the
role's name — no placeholder, because by the time the verb returns the artifact already exists:

```go
f.Arrange().
NewClient[Buyer](WithName("Acme Corporation"))

// any later phase:
f.Of[Buyer]() // the client, as a value
```

See [Tokens](/concepts/tokens/). Arrange fails **hard** — a broken setup makes every later step
meaningless, so the first failing step ends the test.

</TabItem>
</Tabs>

## Act

Act is the one thing under test — and, like Arrange and Inspect, it's a **first-class phase**. An Act
operation is reusable **vocabulary** too, and it produces the artifact the later Inspect observes.

<Tabs syncKey="lang">
<TabItem label="C#">

An Act comes in three flavors, depending on what (if anything) the operation hands back:

```csharp
// Void — the operation's effects are observed downstream (e.g. a message is emitted onto a bus).
await stage.Act().Then(host => host.ExecuteAsync<IProducer>(p => p.ProduceAsync(topic, message)));

// Return — the operation returns its artifact directly (the familiar `var result = await ...`).
var result = await stage.Act().Returning(host =>
host.ExecuteAsync<SaveClientHandler, SaveResult>(handler => handler.Handle(command)));

// Capture — the operation threads its result forward through an out capture, exactly like Arrange.
await stage.Act().SaveClient(out var result, command);
```

`Execute`/`ExecuteAsync` come in 1-to-4-service arities, so an Act can pull several collaborators at once.

</TabItem>
<TabItem label="Go">

Eager execution collapses C#'s three flavors to one: the verb is concrete, so it names its own return
type and simply hands the result back:

```go
func (a Act) SaveClient(cmd SaveCommand) SaveResult {
a.Helper()

return a.Get(func(h mokkit.Host) (SaveResult, error) {
return h.Resolve[*SaveClientHandler]().Handle(h.Context(), cmd)
})
}

// in the test:
result := f.Act().SaveClient(cmd)
```

Inside a step, `h.Resolve[T]()` pulls any collaborator from the stage. Act fails hard, like Arrange:
`Get` fails the test on an error. A test about a refusal wants the error as its artifact, which is what
`Try` hands back as an `Outcome[T]`, and `Attempt` for an operation that returns only an error.

</TabItem>
</Tabs>

As with Arrange and Inspect, teams give their Acts domain names — `Act.CreateClient(...)`,
`Act.ProduceStatusChanged(...)` — so the test body reads as the story it tells. When a test is a whole
*sequence* of Act steps interleaved with checks, see **[Scenario tests](/concepts/scenarios/)**.

## Inspect

Inspect steps **read** the world — query the database, verify a mock, poll an endpoint — and assert on
it. They must not mutate state.

<Tabs syncKey="lang">
<TabItem label="C#">
```csharp
await stage.Inspect()
.SaveResult(result).IsSuccess() // a value scope over the result
.DbClientExists(id) // reads the database
.EventPublished("clients.created", id); // verifies a mock
```

Inspect also offers two power tools, covered in the guides:

- **Value scopes** — `ThenValueScope(value)` opens a focused block of assertions over one value.
- **Parallel inspects** — `ThenAll(...)` runs independent observations concurrently while the chain stays
readable.

</TabItem>
<TabItem label="Go">
```go
f.Inspect().
SaveResult(result).IsSuccess(). // a value scope over the result
DbClientExists(result.ID). // reads the database
All( // independent observations, concurrently
eventPublished(f, "clients.created", result.ID),
auditRowWritten(f, result.ID),
)
```

Inspect fails **soft**: a failing observation reports and the chain keeps going, so one run tells you
everything that is wrong — what C# reaches for `Assert.Multiple` to get. Value scopes are ordinary
vocabulary types carrying the value as a field; `All` runs branches concurrently, and `mokkit.Group`
turns several steps into one branch.

</TabItem>
</Tabs>

## The discipline: produce vs observe

One rule keeps tests trustworthy and reads across the whole suite:

> **Arrange and Act *produce* artifacts. Inspect only *observes* them.**

If setting something up needs a side effect, it belongs in Arrange (or is the Act itself). Inspect never
creates or changes state — so you can always trust that a failing Inspect is reporting on what Act did, not
on something Inspect itself caused.

The verbs in these three chains — `SaveResult`, `DbClientExists`, `EventPublished` — are the heart of the
matter. They're your project's **[test vocabulary](/concepts/vocabulary/)**.
6 changes: 6 additions & 0 deletions docs/src/content/docs/concepts/captures.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ title: "Captures: Capture vs Trapture"
description: How an artifact created in one phase threads into the next while the whole chain stays deferred.
---

:::note[Coming from Go?]
Captures exist because C# chains are **deferred** — a placeholder must stand in for a value that will
only exist when the chain is awaited. Go Mokkit's chains are eager, so captures have no Go equivalent;
artifacts travel through typed [tokens](/concepts/tokens/) instead.
:::

Arrange and Act are **deferred**: `.Then(...)` only *records* a step; nothing runs until you `await`. That
raises a question — if the client isn't created until the chain runs, how does a later step refer to it? The
answer is a **capture**: a typed placeholder handed back immediately and filled when the step runs.
Expand Down
Loading
Loading