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
45 changes: 45 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Docs

on:
push:
branches: [ main ]
paths:
- 'docs/**'
- 'src/**' # rebuild the API reference when the library changes
- '.github/workflows/docs.yml'
workflow_dispatch:

jobs:
build-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# .NET 8 runs the DocFX tool natively; .NET 10 matches global.json for building the projects.
- uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x

- uses: actions/setup-node@v4
with:
node-version: 22

- name: Generate API reference (DocFX → docs/public/api)
run: |
dotnet tool install -g docfx
docfx docs/docfx.json

- name: Build the docs site (Starlight, includes /api)
working-directory: docs
run: |
npm ci
npm run build

- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy docs/dist --project-name=mokkit-docs
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -402,3 +402,6 @@ FodyWeavers.xsd

# Visual Studio Code
.vscode

# MacOS
.DS_Store
71 changes: 70 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,70 @@
# mokkit
# Mokkit

[![NuGet](https://img.shields.io/nuget/vpre/Mokkit.svg)](https://www.nuget.org/packages/Mokkit)
[![CI](https://github.com/GrafGenerator/mokkit/actions/workflows/ci.yml/badge.svg)](https://github.com/GrafGenerator/mokkit/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

**Write tests that read like a story in your domain's language — as ordinary, compilable C#.**

Mokkit gives your tests the readability of BDD tools like Cucumber/SpecFlow, but with **no DSL**: no feature
files, no step bindings, no runtime glue. The "steps" are just C# extension methods you author — your
project's testing vocabulary — so you keep full IDE support (autocomplete, go-to-definition, refactoring) and
a test that doesn't make sense simply won't compile.

It's a thin orchestration layer, not a framework: run it inside xUnit / NUnit / MSTest, mock with
Moq / NSubstitute / FakeItEasy, and wire with Microsoft DI / Autofac / Castle Windsor (or the dependency-free
Bag). The same Arrange / Act / Inspect vocabulary scales from a mocked unit test to a full Testcontainers
end-to-end run.

```csharp
// A Mokkit test reads like the scenario it describes.
[Fact]
public async Task Suspending_a_client_reflects_everywhere()
{
await Arrange
.NewClient(out var clientId, WithName("Acme Corporation"))
.CacheHasClient(clientId);

await Act(clientId, ClientStatus.Suspended);

await Inspect
.ApiClientEventually(clientId, c => c.Status == Suspended)
.DbClient(clientId, c => c!.Status.ShouldBe(Suspended))
.EventPublished("clients.updated", clientId);
}
```

`NewClient`, `CacheHasClient`, `ApiClientEventually`, `DbClient`, `EventPublished` aren't Mokkit APIs — they're
your verbs. Mokkit provides the Arrange / Act / Inspect shape and the machinery underneath.

## Install

Core + a DI adapter + a mock adapter (prerelease for now):

```bash
dotnet add package Mokkit --prerelease
dotnet add package Mokkit.Containers.Microsoft.Extensions.DependencyInjection --prerelease
dotnet add package Mokkit.Containers.NSubstitute --prerelease
```

## Packages

| Package | Purpose |
| --- | --- |
| `Mokkit` | Core: Stage, Arrange/Act/Inspect, captures, the `[MokkitCapture]` source generator |
| `Mokkit.Containers.Microsoft.Extensions.DependencyInjection` · `.Autofac` · `.CastleWindsor` | DI container adapters |
| `Mokkit.Containers.Moq` · `.NSubstitute` · `.FakeItEasy` | Mock library adapters |
| `Mokkit.Containers.Bag` | Dependency-free "hold a few instances" container |

## Documentation

Full guides, concepts and API reference: **[mokkit.net](https://mokkit.net)**

- [Introduction](https://mokkit.net/introduction/) · [Why Mokkit? (vs BDD/DSL)](https://mokkit.net/why-mokkit/) · [Quickstart](https://mokkit.net/quickstart/)
- [Building your test vocabulary](https://mokkit.net/concepts/vocabulary/) — the idea Mokkit is built around

A worked, three-tier example (unit / integration / e2e) lives in [`example/Example1`](example/Example1).

## License

[MIT](LICENSE) © Nikita Ivanov
13 changes: 13 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# DocFX-generated API reference (built in CI into public/api)
public/api/
# environment
.env
.env.production
# DocFX intermediate metadata
_api/
12 changes: 12 additions & 0 deletions docs/apidoc/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Mokkit API reference

Auto-generated from the XML documentation comments in the Mokkit source. Use the sidebar to browse
namespaces and types, or start with the essentials:

- **Captures** — [`Mokkit.Capture<T>`](Mokkit.Capture-1.html), [`Mokkit.Trapture<T>`](Mokkit.Trapture-1.html), [`Mokkit.MokkitCaptureAttribute`](Mokkit.MokkitCaptureAttribute.html)
- **Arrange** — [`Mokkit.Arrange.ITestArrange`](Mokkit.Arrange.ITestArrange.html), [`Mokkit.Arrange.EnsureArrangeExtensions`](Mokkit.Arrange.EnsureArrangeExtensions.html)
- **Inspect** — [`Mokkit.Inspect.ITestInspect`](Mokkit.Inspect.ITestInspect.html), [`Mokkit.Inspect.EnsureInspectExtensions`](Mokkit.Inspect.EnsureInspectExtensions.html)
- **Stage** — [`Mokkit.Suite.TestStageSetup`](Mokkit.Suite.TestStageSetup.html), [`Mokkit.Suite.TestStage`](Mokkit.Suite.TestStage.html)
- **Containers** — [`Mokkit.Containers.IDependencyContainerBuilder`](Mokkit.Containers.IDependencyContainerBuilder.html), [`Mokkit.Containers.IStageResolve`](Mokkit.Containers.IStageResolve.html)

For guides and concepts, head back to the [Mokkit documentation](/).
82 changes: 82 additions & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';

// The project's custom domain — drives canonical URLs and the sitemap.
export default defineConfig({
site: 'https://mokkit.net',
integrations: [
starlight({
title: 'Mokkit',
description: "Write tests that read like a story in your domain's language — as plain, compilable C#.",
social: [
{ icon: 'github', label: 'GitHub', href: 'https://github.com/GrafGenerator/mokkit' },
],
editLink: {
baseUrl: 'https://github.com/GrafGenerator/mokkit/edit/main/docs/',
},
sidebar: [
{
label: 'Start here',
items: [
{ label: 'Introduction', slug: 'introduction' },
{ label: 'Why Mokkit?', slug: 'why-mokkit' },
{ label: 'Installation', slug: 'installation' },
{ label: 'Quickstart', slug: 'quickstart' },
],
},
{
label: 'Core concepts',
items: [
{ label: 'Arrange / Act / Inspect', slug: 'concepts/aai' },
{ 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: 'Containers & the mock→DI bridge', slug: 'concepts/containers' },
],
},
{
label: 'Guides',
items: [
{ label: 'Unit-test a service with a mock', slug: 'guides/unit-mocked-dependency' },
{ label: 'Pick a mock library', slug: 'guides/mock-libraries' },
{ label: 'Wire a real DI container', slug: 'guides/real-di-container' },
{ label: 'The Bag container', slug: 'guides/bag-container' },
{ label: 'Integration-test a database', slug: 'guides/integration-database' },
{ label: 'Full black-box E2E', slug: 'guides/end-to-end' },
{ 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: '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: 'Extending',
items: [
{ label: 'Advanced vocabulary techniques', slug: 'guides/advanced-vocabulary' },
{ label: 'Write a custom container adapter', slug: 'guides/custom-container-adapter' },
],
},
{
label: 'Reference',
items: [
{ 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/' },
],
},
],
}),
],
});
39 changes: 39 additions & 0 deletions docs/docfx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"metadata": [
{
"src": [
{
"src": "../src",
"files": [
"Mokkit/Mokkit.csproj",
"Mokkit.Containers.Common/Mokkit.Containers.Common.csproj",
"Mokkit.Containers.Bag/Mokkit.Containers.Bag.csproj",
"Mokkit.Containers.Moq/Mokkit.Containers.Moq.csproj",
"Mokkit.Containers.NSubstitute/Mokkit.Containers.NSubstitute.csproj",
"Mokkit.Containers.FakeItEasy/Mokkit.Containers.FakeItEasy.csproj",
"Mokkit.Containers.Autofac/Mokkit.Containers.Autofac.csproj",
"Mokkit.Containers.CastleWindsor/Mokkit.Containers.CastleWindsor.csproj",
"Mokkit.Containers.Microsoft.Extensions.DependencyInjection/Mokkit.Containers.Microsoft.Extensions.DependencyInjection.csproj"
]
}
],
"dest": "_api",
"outputFormat": "apiPage"
}
],
"build": {
"content": [
{ "files": ["**/*.{yml,md}"], "src": "_api", "dest": "." },
{ "files": ["index.md"], "src": "apidoc", "dest": "." }
],
"output": "public/api",
"template": ["default", "modern"],
"globalMetadata": {
"_appName": "Mokkit",
"_appTitle": "Mokkit API reference",
"_appFooter": "Mokkit — <a href=\"/\">back to docs</a>",
"_disableContribution": true,
"pdf": false
}
}
}
Loading
Loading