Skip to content

Specifications

A good spec should read like the behavior it protects. When a new developer opens the test project, they should not have to reverse-engineer a large FooTests file. They should be able to follow the folders: for this class, when this behavior happens, under this condition, these facts should be true.

Cratis.Specifications is the small BDD-style layer we use for that shape. It keeps the familiar xUnit or NUnit runner, but adds the Specification by Example structure: Establish() for given, Because() for when, [Fact] or [Test] methods for then, and Destroy() for cleanup.

PackageUse it for
Cratis.SpecificationsShared lifecycle discovery and helpers such as Catch.Exception.
Cratis.Specifications.XUnitxUnit Specification base class plus Should* assertion extensions over xUnit assertions.
Cratis.Specifications.NUnitNUnit Specification base class plus Should* assertion extensions over NUnit assertions.

Most Cratis repositories use central package versions. In that setup, a spec project only needs the package reference:

<ItemGroup>
<PackageReference Include="Cratis.Specifications.XUnit" />
</ItemGroup>

Use the NUnit package when the project runs on NUnit:

<ItemGroup>
<PackageReference Include="Cratis.Specifications.NUnit" />
</ItemGroup>

Both packages use the Cratis.Specifications namespace.

The lifecycle methods are discovered by convention. They are optional, take no arguments, and can be void or Task.

MethodBDD wordPurpose
Establish()GivenBuild the context: inputs, services, mocks, existing state.
Because()WhenPerform the single behavior under specification.
[Fact] or [Test]ThenAssert one fact about the result.
Destroy()CleanupRelease resources created by the spec.

The base class runs lifecycle methods across the inheritance chain, so reusable contexts can live in given/ classes and still participate in the same Establish() / Because() flow.

Start with the behavior. A security service authenticates a user and returns a token with the user’s role and session id.

using Cratis.Specifications;
using Xunit;
namespace MyApp.Security.for_SecurityService;
public class when_authenticating_an_admin_user : Specification
{
SecurityService _subject = default!;
UserToken _result = default!;
void Establish() =>
_subject = new SecurityService();
void Because() =>
_result = _subject.Authenticate("admin", "correct-password");
[Fact] void should_indicate_the_users_role() =>
_result.Role.ShouldEqual(Roles.Admin);
[Fact] void should_have_a_session_id() =>
_result.SessionId.ShouldNotBeNull();
}

The class name, folder path, lifecycle methods, and facts form the sentence:

for_SecurityService / when_authenticating_an_admin_user / should_indicate_the_users_role.

Use Catch.Exception when the behavior is expected to fail. That keeps the exception as the observable result of Because().

using Cratis.Specifications;
using Xunit;
namespace MyApp.Security.for_SecurityService;
public class when_authenticating_without_a_user : Specification
{
SecurityService _subject = default!;
Exception _result = default!;
void Establish() =>
_subject = new SecurityService();
void Because() =>
_result = Catch.Exception(() => _subject.Authenticate(null!, null!));
[Fact] void should_require_a_user() =>
_result.ShouldBeOfExactType<UserMustBeSpecified>();
}

For async behavior, Catch.Exception(Func<Task>) returns the exception from the awaited callback.

When several specs share setup, move the setup into a given/ context and inherit from it.

for_SecurityService/
given/
no_user_authenticated.cs
when_authenticating_an_admin_user.cs
when_authenticating_without_a_user.cs
using Cratis.Specifications;
namespace MyApp.Security.for_SecurityService.given;
public class no_user_authenticated : Specification
{
protected SecurityService _subject = default!;
void Establish() =>
_subject = new SecurityService();
}
using Cratis.Specifications;
using Xunit;
namespace MyApp.Security.for_SecurityService;
public class when_authenticating_without_a_user : given.no_user_authenticated
{
Exception _result = default!;
void Because() =>
_result = Catch.Exception(() => _subject.Authenticate(null!, null!));
[Fact] void should_require_a_user() =>
_result.ShouldBeOfExactType<UserMustBeSpecified>();
}

Use reusable contexts for meaningful givens, not just to avoid a few repeated lines. The folder should still read like documentation.

The BDD shape is the same with NUnit. The visible difference is the assertion method attribute.

using Cratis.Specifications;
using NUnit.Framework;
namespace MyApp.Security.for_SecurityService;
public class when_authenticating_an_admin_user : Specification
{
SecurityService _subject = default!;
UserToken _result = default!;
void Establish() =>
_subject = new SecurityService();
void Because() =>
_result = _subject.Authenticate("admin", "correct-password");
[Test] public void should_indicate_the_users_role() =>
_result.Role.ShouldEqual(Roles.Admin);
}
xUnitNUnit
Reference Cratis.Specifications.XUnit.Reference Cratis.Specifications.NUnit.
Facts use [Fact].Facts use [Test].
The package integrates with xUnit through IAsyncLifetime.The package integrates with NUnit through [TestFixture], [OneTimeSetUp], and [OneTimeTearDown].
Fact methods can be non-public in the Cratis style.NUnit test methods should be public.

The framework-specific packages expose assertion extension methods with the same names, so the spec reads the same whether it runs on xUnit or NUnit.

AssertionUse it for
ShouldEqual(expected) / ShouldNotEqual(expected)Value equality.
ShouldBeTrue() / ShouldBeFalse()Boolean facts.
ShouldBeNull() / ShouldNotBeNull()Null checks.
ShouldBeSame(expected) / ShouldNotBeSame(expected)Reference identity.
ShouldBeOfExactType<T>()Exact runtime type.
ShouldBeAssignableFrom<T>()Assignable runtime type.
ShouldContain(...) / ShouldNotContain(...)Strings, collections, and dictionaries.
ShouldBeEmpty() / ShouldNotBeEmpty()Collections.
ShouldContainSingleItem()Collections that should have exactly one item.
ShouldBeGreaterThan(...), ShouldBeLessThan(...), and range helpersComparable values.

Use raw framework assertions only when there is no Should* helper for the fact you need.

Specs favor readable names over normal production-code naming conventions.

LevelPatternExample
Unit folderfor_<TypeOrConcept>for_SecurityService
Shared contextgiven/<context>.csgiven/no_user_authenticated.cs
Behavior folderwhen_<action>when_authenticating
Condition fileand_<condition>.cs or with_<state>.csand_the_user_is_an_admin.cs
Simple behavior filewhen_<action>.cswhen_authenticating_without_a_user.cs
Fact methodshould_<outcome>should_require_a_user

This naming deliberately triggers some analyzer and style warnings. Spec projects commonly suppress warnings such as underscore naming and missing XML documentation for spec classes.

Use Specifications when the behavior benefits from an executable sentence:

  • Business rules.
  • Domain services.
  • Command handlers and validation.
  • Event-sourced slices.
  • Projection, reducer, and reactor behavior.
  • Edge cases that future readers need to understand.

Use plain xUnit or NUnit tests when the test is pure infrastructure smoke, generated-code verification, or a very small assertion where the BDD structure would add noise.

For Cratis application tests, Specifications are usually the outer shape, while Arc and Chronicle provide the scenario objects inside it:

  • CommandScenario<TCommand> for Arc command behavior.
  • EventScenario for event append and constraint behavior.
  • ReadModelScenario<TReadModel> for projections and reducers.
  • ReactorScenario<TReactor> for event-driven side effects.