How to structure a Mokkit test project
Mokkit doesn’t prescribe a folder layout, but a good one makes your vocabulary discoverable and your fixtures obvious. This is the structure the example uses across three suites.
Colocate vocabulary with what it describes
Section titled “Colocate vocabulary with what it describes”Keep the verbs for a feature (or system-under-test) next to that feature’s tests, in the same namespace — so
they’re in scope with no extra usings. The canonical unit of organisation is a folder holding four files:
Features/Client/SaveClient/├── ArrangeSaveClient.cs # arrange verbs (build commands, seed state)├── ActSaveClient.cs # act verbs (the operation under test)├── InspectSaveClient.cs # inspect verbs (observe results & effects)└── SaveClientCommandHandlerTests.csArrange<Feature>.cs / Act<Feature>.cs / Inspect<Feature>.cs mirror the three phases. A test in this folder
reads as await Arrange.…, await Act.…, await Inspect.… using words defined right beside it.
Genuinely cross-feature verbs (a Clock/Ids arrange, a shared DbClient seed) go in a root-namespace
Helpers/ folder, so enclosing-namespace lookup makes them visible to every feature test.
A base fixture builds the Stage once
Section titled “A base fixture builds the Stage once”Put the Stage composition in a base fixture that builds once and exposes the three phases as properties:
protected TestStage Stage { get; }protected ITestArrange Arrange => Stage.Arrange();protected ITestAct Act => Stage.Act();protected ITestInspect Inspect => Stage.Inspect();Each test enters a fresh stage over that fixed composition and disposes it afterwards. This is identical across xUnit / NUnit / MSTest — only the fixture attributes differ.
One container build → one fixture per SUT
Section titled “One container build → one fixture per SUT”Because containers are built once, a type is either the real SUT or a mock in a given composition — not both. So each system-under-test gets its own small fixture, declaring its real type plus mocks for its direct dependencies:
public sealed class ProcessorFixture : BaseStageFixture{ protected override void ConfigureSubstitutes(ISubstituteCollection s) { s.AddSubstitute<IRequestHandler<SaveClientCommand, SaveClientCommandResult>>(); s.AddSubstitute<IKafkaEventPublisher>(); } protected override void ConfigureServices(IServiceCollection services) => services.AddScoped<IClientStatusChangedProcessor, ClientStatusChangedProcessor>();}The discipline that keeps it honest
Section titled “The discipline that keeps it honest”One rule underpins the whole layout:
Arrange and Act produce artifacts. Inspect only observes them — reads are fine, mutations are not.
No raw setup or assertions in a test body: every value is a business-named arrange, every check a business-named inspect, and the act returns (or captures) the artifact the inspects observe. If an inspect is doing the thing under test, lift it into Act; if an act asserts, move that into Inspect.
Three real layouts
Section titled “Three real layouts”The example organises each suite by whatever axis fits it — all following the rules above:
Unit.Tests/ # organised BY SUT/concern (portability proof: xUnit + NSubstitute)├── BaseStageFixture.cs BaseUnitTest.cs├── Containers/ # a bring-your-own NSubstitute adapter├── Cache/ { CacheServiceFixture, ArrangeCache, InspectCache, …Tests }├── Validation/ { ValidatorFixture, ArrangeCommand, InspectValidation, …Tests }└── Messaging/ # one concern, several SUTs → subfolder per SUT ├── Processor/ { ProcessorFixture, ArrangeProcessor, InspectProcessor, …Tests } ├── Publisher/ { … } └── Consumer/ { … }
Integration.Tests/ # organised BY FEATURE (NUnit + Moq + Postgres/Respawn)├── BaseIntegrationTest.cs├── Helpers/ { ArrangeClient, InspectClient, VerifierSetup } # cross-feature, root namespace└── Features/Client/ ├── GetClient/ { ArrangeGetClient, ActGetClient, InspectGetClient, …Tests } └── SaveClient/ { ArrangeSaveClient, ActSaveClient, InspectSaveClient, …Tests, *.verified.txt }
E2E.Tests/ # organised BY EXTERNAL SURFACE (xUnit + Testcontainers, nothing mocked)├── BaseE2ETest.cs E2EStack.cs E2ECollection.cs KafkaProbe.cs Poll.cs├── Clients/ { ClientApi, ArrangeClientApi, ArrangeMessages, ActClientApi,│ InspectClientApi, *FlowTests, ClientLifecycleScenarioTests }└── Contracts/ # suite-owned wire DTOs — not the service's internal typesUnit organises by SUT (one fixture each), integration by feature (cross-feature helpers hoisted to the root), E2E by external surface (infra plumbing at the root, its own black-box contracts). Same Mokkit primitives throughout — only the surrounding stack changes.
- Conventions cheat-sheet — the one-screen version of this.
- Building your test vocabulary — what fills the
Arrange*/Act*/Inspect*files.