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
2 changes: 2 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
<!-- Packaging -->
<Version>1.0.0</Version>
<Copyright>Copyright Cratis</Copyright>
<Description>Specification by Example (BDD) for .NET - Given/When/Then specifications in the style of Machine.Specifications, with support for xUnit and NUnit.</Description>
<PackageTags>bdd;specification-by-example;testing;xunit;nunit;mspec;given-when-then;cratis</PackageTags>
<Authors>all contributors</Authors>
<DebugType>portable</DebugType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
69 changes: 47 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
# Specifications
# Cratis Specifications

[![Build](https://github.com/cratis/Specifications/actions/workflows/build.yml/badge.svg)](https://github.com/cratis-system/Specifications/actions/workflows/build.yml)
[![Nuget](https://img.shields.io/nuget/v/cratis.specifications.xunit)](http://nuget.org/packages/cratis.specifications.xunit)
[![Nuget](https://img.shields.io/nuget/v/cratis.specifications.nunit)](http://nuget.org/packages/cratis.specifications.nunit)
**Specification by Example (BDD) for .NET — Given/When/Then specs with xUnit and NUnit, in the style of [Machine.Specifications](https://github.com/machine/machine.specifications).**

This project represents a way to do Specification by Example - BDD style inspired by
the conciseness of [Machine.Specifications](https://github.com/machine/machine.specifications).
[![Build](https://github.com/Cratis/Specifications/actions/workflows/build.yml/badge.svg)](https://github.com/Cratis/Specifications/actions/workflows/build.yml)
[![Nuget](https://img.shields.io/nuget/v/cratis.specifications.xunit?label=Cratis.Specifications.XUnit&logo=nuget)](https://www.nuget.org/packages/cratis.specifications.xunit)
[![Nuget](https://img.shields.io/nuget/v/cratis.specifications.nunit?label=Cratis.Specifications.NUnit&logo=nuget)](https://www.nuget.org/packages/cratis.specifications.nunit)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Discord](https://img.shields.io/discord/1182595891576717413?label=Discord&logo=discord&logoColor=white)](https://discord.gg/kt4AMpV8WV)

It has support for XUnit and NUnit frameworks for testing.
Cratis Specifications brings Specification by Example — behavior-driven development (BDD) with
**given**, **when**, **then** — to .NET test projects, keeping the concise structure and syntax
popularized by Machine.Specifications while running on standard test frameworks. That means full
tooling support across IDEs, editors, and CI, with your choice of xUnit or NUnit as the runner.
The background and motivation are described in
[this article](https://www.ingebrigtsen.info/2021/09/05/specifications-in-xunit/).

Packages:

[XUnit](http://nuget.org/packages/cratis.specifications.xunit)
[NUnit](http://nuget.org/packages/cratis.specifications.nunit)

The motivation behind is years of work with Machine.Specifications and the wish to maintain
the approach, structure and syntax - but get more love from tooling across different platforms
and IDEs and editors; read more [here](https://www.ingebrigtsen.info/2021/09/05/specifications-in-xunit/).
- [Cratis.Specifications.XUnit](https://www.nuget.org/packages/cratis.specifications.xunit)
- [Cratis.Specifications.NUnit](https://www.nuget.org/packages/cratis.specifications.nunit)

## What does it do?

Expand All @@ -26,14 +28,14 @@ system. What this particular library delivers is a way to do these and also keep
world of having isolated specifications and not have typically a **FooTests** and dump all your tests for the unit `Foo` in
it.

The library supports my convention lifecycle methods `Establish()`, `Because()` and `Destroy()`. There is no virtual method
The library supports the convention lifecycle methods `Establish()`, `Because()` and `Destroy()`. There is no virtual method
to override, just match the expected signatures:

| Signature | Purpose |
| --------- | ------- |
| void Establish() | Establishes the current context - **given** / **arrange** |
| void Because() | Triggers the behavior being specified - **when** / **act** |
| void Destroy() | Tears down the context |
| void Destroy() | Tears down the context |

If your specification requires to run in an async context, it also supports the following:

Expand Down Expand Up @@ -79,18 +81,18 @@ end up getting a lot of warnings. These can be turned off by adding a **NoWarn**
</PropertyGroup>
```

| Warning | Description |
| Warning | Description |
| ------- | ----------- |
| [CA1707](https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1707) | Identifiers should not contain underscores |
| [CA1051](https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/CA1051) | Do not declare visible instance fields |
| [CS1591](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs1591) | Missing XML comment for publicly visible type or member 'Type_or_Member' |
| [IDE0051](https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0051) | Remove unused private member |
| [IDE1006](https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/naming-rules#rule-id-ide1006-naming-rule-violation) | Naming rule violation |
| [RCS1213](https://github.com/JosefPihrt/Roslynator/blob/master/docs/analyzers/RCS1213.md) | Remove unused member declaration|
| [IDE0051](https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0051) | Remove unused private member |
| [IDE1006](https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/naming-rules#rule-id-ide1006-naming-rule-violation) | Naming rule violation |
| [RCS1213](https://github.com/JosefPihrt/Roslynator/blob/master/docs/analyzers/RCS1213.md) | Remove unused member declaration|

If you're using static code analysis and stylecop and have turned on all rules by default, you might also encounter the following that you want to turn off:

| Warning | Description |
| Warning | Description |
| ------- | ----------- |
| [SA1633](https://documentation.help/StyleCop/SA1636.html) | File header copyright text must match |
| [SA1649](https://documentation.help/StyleCop/SA1649.html) | File name must match type name |
Expand All @@ -99,9 +101,9 @@ If you're using static code analysis and stylecop and have turned on all rules b
| [SA1502](https://documentation.help/StyleCop/SA1502.html) | Element must not be on a single line |
| [SA1134](https://documentation.help/StyleCop/SA1134.html) ||

Depending on your solution, you might want to consider suppressnig the following.
Depending on your solution, you might want to consider suppressing the following.

| Warning | Description |
| Warning | Description |
| ------- | ----------- |
| [RCS1090](https://github.com/JosefPihrt/Roslynator/blob/master/docs/analyzers/RCS1090.md) | Add call to 'ConfigureAwait'.|

Expand Down Expand Up @@ -195,3 +197,26 @@ class no_user_authenticated

}
```

## Related projects

Specifications pairs naturally with [Synopsis](https://github.com/Cratis/Synopsis), which turns
your specs into living documentation, and it is the testing style used throughout the Cratis
stack — including [Chronicle](https://github.com/Cratis/Chronicle), the event-sourcing database
and runtime, and [Arc](https://github.com/Cratis/Arc), the CQRS application framework for
ASP.NET Core.

## The Cratis ecosystem

This project is part of [Cratis](https://www.cratis.io) — free, MIT-licensed tools for building event-sourced and CQRS applications.

- **[Chronicle](https://github.com/Cratis/Chronicle)** — event-sourcing database and runtime. Orleans-based kernel, pluggable storage (MongoDB default; PostgreSQL, SQL Server, SQLite, in-memory), language-agnostic gRPC contracts. [Docs](https://www.cratis.io/chronicle/)
- **Chronicle clients** — first-class [.NET SDK](https://github.com/Cratis/Chronicle), plus [TypeScript](https://github.com/Cratis/Chronicle.TypeScript), [Kotlin/Java](https://github.com/Cratis/Chronicle.Kotlin), and [Elixir](https://github.com/Cratis/Chronicle.Elixir); [Python](https://github.com/Cratis/Chronicle.Python) coming soon (pre-alpha). AI agents connect through the [Chronicle MCP server](https://github.com/Cratis/Chronicle.Mcp).
- **[Arc](https://github.com/Cratis/Arc)** — opinionated CQRS framework for ASP.NET Core with commands, queries, validation, authorization, and TypeScript proxy generation. Works without event sourcing. [Docs](https://www.cratis.io/arc/)
- **[Components](https://github.com/Cratis/Components)** — React components aligned with Arc patterns. [Docs](https://www.cratis.io/components/)
- **[CLI](https://github.com/Cratis/cli) + Workbench** — inspect and diagnose Chronicle from the terminal or the browser. [Docs](https://www.cratis.io/cli/)
- **Model-first layer (experimental)** — [Studio](https://github.com/Cratis/Studio), [Screenplay](https://github.com/Cratis/Screenplay), [Stage](https://github.com/Cratis/Stage), [Scene](https://github.com/Cratis/Scene), [Prologue](https://github.com/Cratis/Prologue)
- **Supporting** — [Fundamentals](https://github.com/Cratis/Fundamentals), [Specifications](https://github.com/Cratis/Specifications) (this project), [Synopsis](https://github.com/Cratis/Synopsis), [Lens](https://github.com/Cratis/Lens), [Narrator](https://github.com/Cratis/Narrator), and free [AI tooling](https://github.com/Cratis/AI) (preview); [Ensemble](https://github.com/Cratis/Ensemble) coming soon (pre-release)
- **[Samples](https://github.com/Cratis/Samples)** — runnable event sourcing and CQRS samples for the whole stack

Everything Cratis publishes today is MIT licensed and free to use.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class BecauseMustNotBeOnGivenContextAnalyzer : DiagnosticAnalyzer
description: "A 'given' context captures the world before the action under test (the Given). Placing a Because() on it runs the action for every specification that derives from the context, which blurs what is being tested and can trigger the action multiple times. Keep 'given' contexts to Establish() only and move Because() into the concrete 'when_' specification.");

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(_rule);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [_rule];

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class DoNotCallBaseLifecycleMethodAnalyzer : DiagnosticAnalyzer
description: "Cratis specifications discover and run Establish/Because/Destroy across the whole inheritance chain automatically, base-first. Calling 'base.Establish()' (or Because/Destroy) explicitly runs that context's setup a second time, which can corrupt state or double-invoke the action under test. Remove the explicit base call.");

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(_rule);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [_rule];

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class FactMethodShouldNamingAnalyzer : DiagnosticAnalyzer
description: "Cratis specifications read as behavioral facts (Given/When/Then). A test method ([Fact], [Theory], [Test], or [TestCase]) inside a Specification-derived class should start with 'should_' so the test output reads as a sentence, for example 'should_reject_the_command'. This keeps the BDD readability that is the whole point of the framework.");

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(_rule);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [_rule];

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class FactOnGivenBaseClassAnalyzer : DiagnosticAnalyzer
description: "A 'given' type establishes a reusable context (Given) and is inherited by 'when_' specifications; it is not itself a runnable specification. A test method ([Fact], [Theory], [Test], or [TestCase]) placed on a 'given' context is silently never executed, producing a real coverage gap. Move the assertion into a 'when_' specification that derives from the context.");

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(_rule);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [_rule];

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class OneSpecificationPerFileAnalyzer : DiagnosticAnalyzer
description: "Cratis specifications map one behavior to one file (for_/when_/and_). Declaring more than one Specification-derived class in a single file hides scenarios and breaks the navigable one-file-per-behavior convention. Shared context types in a 'given' namespace and '*SpecHelpers' types are exempt.");

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(_rule);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [_rule];

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class ShouldMethodMustBeTestMethodAnalyzer : DiagnosticAnalyzer
description: "A method named 'should_*' inside a Specification-derived class reads as a behavioral fact, but without a test attribute ([Fact], [Theory], [Test], or [TestCase]) it is just an ordinary private method that the runner never executes. This silently drops the assertion and creates a false sense of coverage. Add the missing test attribute, or rename the method if it is a helper.");

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(_rule);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [_rule];

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class SpecificationMustBePublicAnalyzer : DiagnosticAnalyzer
description: "The test runner only discovers publicly accessible test classes. A Specification-derived class that declares test methods but is internal (or nested inside a non-public type) is silently skipped, producing a false sense of coverage. Make the specification (and any type containing it) public.");

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(_rule);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [_rule];

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
Expand Down
2 changes: 1 addition & 1 deletion Source/CodeAnalysis/SpecificationFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public static class SpecificationFacts
/// <summary>
/// The convention-discovered lifecycle method names.
/// </summary>
public static readonly ImmutableArray<string> LifecycleMethodNames = ImmutableArray.Create("Establish", "Because", "Destroy");
public static readonly ImmutableArray<string> LifecycleMethodNames = ["Establish", "Because", "Destroy"];

static readonly HashSet<string> _testMethodAttributeNames = new(System.StringComparer.Ordinal)
{
Expand Down
1 change: 1 addition & 0 deletions Source/NUnit/NUnit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<AssemblyName>Cratis.Specifications.NUnit</AssemblyName>
<RootNamespace>Cratis.Specifications</RootNamespace>
<IsPackable>true</IsPackable>
<Description>Specification by Example (BDD) for .NET with NUnit - Given/When/Then specifications in the style of Machine.Specifications, running on the NUnit test framework.</Description>
<Nullable>enable</Nullable>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>
Expand Down
1 change: 1 addition & 0 deletions Source/XUnit/XUnit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<AssemblyName>Cratis.Specifications.XUnit</AssemblyName>
<RootNamespace>Cratis.Specifications</RootNamespace>
<IsPackable>true</IsPackable>
<Description>Specification by Example (BDD) for .NET with xUnit - Given/When/Then specifications in the style of Machine.Specifications, running on the xUnit test framework.</Description>
<Nullable>enable</Nullable>
</PropertyGroup>

Expand Down
Loading