From af02bc928fc8fd216ac51e6da1967eb78a2180cd Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 28 Aug 2026 00:51:05 +0200 Subject: [PATCH 1/4] Modernize README with tagline, badges, and Cratis ecosystem links Lead with a Specification by Example (BDD) tagline, fix the build badge link target (cratis-system -> Cratis), add MIT license and Discord badges, move the introduction to a neutral voice, and add related project and Cratis ecosystem sections linking Synopsis, Chronicle, Arc, and the rest of the stack. --- README.md | 69 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 116c47b..6c18789 100644 --- a/README.md +++ b/README.md @@ -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? @@ -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: @@ -79,18 +81,18 @@ end up getting a lot of warnings. These can be turned off by adding a **NoWarn** ``` -| 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 | @@ -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'.| @@ -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) +- **[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. From 6ac4d1c62cdb0c4ef3ad08820c590736b539167e Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 28 Aug 2026 00:51:05 +0200 Subject: [PATCH 2/4] Add NuGet package descriptions and tags for discoverability Set a shared Description and PackageTags (bdd, specification-by-example, xunit, nunit, mspec, given-when-then) in Directory.Build.props and override the description per test framework in the XUnit and NUnit packages. --- Directory.Build.props | 2 ++ Source/NUnit/NUnit.csproj | 1 + Source/XUnit/XUnit.csproj | 1 + 3 files changed, 4 insertions(+) diff --git a/Directory.Build.props b/Directory.Build.props index c165da6..0df05af 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -31,6 +31,8 @@ 1.0.0 Copyright Cratis + Specification by Example (BDD) for .NET - Given/When/Then specifications in the style of Machine.Specifications, with support for xUnit and NUnit. + bdd;specification-by-example;testing;xunit;nunit;mspec;given-when-then;cratis all contributors portable MIT diff --git a/Source/NUnit/NUnit.csproj b/Source/NUnit/NUnit.csproj index 609f4ea..f274d9e 100644 --- a/Source/NUnit/NUnit.csproj +++ b/Source/NUnit/NUnit.csproj @@ -3,6 +3,7 @@ Cratis.Specifications.NUnit Cratis.Specifications true + Specification by Example (BDD) for .NET with NUnit - Given/When/Then specifications in the style of Machine.Specifications, running on the NUnit test framework. enable net8.0;net9.0;net10.0 diff --git a/Source/XUnit/XUnit.csproj b/Source/XUnit/XUnit.csproj index 62b4357..ec21f92 100644 --- a/Source/XUnit/XUnit.csproj +++ b/Source/XUnit/XUnit.csproj @@ -3,6 +3,7 @@ Cratis.Specifications.XUnit Cratis.Specifications true + Specification by Example (BDD) for .NET with xUnit - Given/When/Then specifications in the style of Machine.Specifications, running on the xUnit test framework. enable From fd85d110291f3486dd9816f657a1c44f4283a67a Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 28 Aug 2026 07:56:35 +0200 Subject: [PATCH 3/4] Use collection expressions in analyzers --- .../Analyzers/BecauseMustNotBeOnGivenContextAnalyzer.cs | 2 +- .../Analyzers/DoNotCallBaseLifecycleMethodAnalyzer.cs | 2 +- Source/CodeAnalysis/Analyzers/FactMethodShouldNamingAnalyzer.cs | 2 +- Source/CodeAnalysis/Analyzers/FactOnGivenBaseClassAnalyzer.cs | 2 +- .../CodeAnalysis/Analyzers/OneSpecificationPerFileAnalyzer.cs | 2 +- .../Analyzers/ShouldMethodMustBeTestMethodAnalyzer.cs | 2 +- .../CodeAnalysis/Analyzers/SpecificationMustBePublicAnalyzer.cs | 2 +- Source/CodeAnalysis/SpecificationFacts.cs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Source/CodeAnalysis/Analyzers/BecauseMustNotBeOnGivenContextAnalyzer.cs b/Source/CodeAnalysis/Analyzers/BecauseMustNotBeOnGivenContextAnalyzer.cs index 8a49097..69ad9dd 100644 --- a/Source/CodeAnalysis/Analyzers/BecauseMustNotBeOnGivenContextAnalyzer.cs +++ b/Source/CodeAnalysis/Analyzers/BecauseMustNotBeOnGivenContextAnalyzer.cs @@ -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."); /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(_rule); + public override ImmutableArray SupportedDiagnostics => [_rule]; /// public override void Initialize(AnalysisContext context) diff --git a/Source/CodeAnalysis/Analyzers/DoNotCallBaseLifecycleMethodAnalyzer.cs b/Source/CodeAnalysis/Analyzers/DoNotCallBaseLifecycleMethodAnalyzer.cs index b4fc091..56a2305 100644 --- a/Source/CodeAnalysis/Analyzers/DoNotCallBaseLifecycleMethodAnalyzer.cs +++ b/Source/CodeAnalysis/Analyzers/DoNotCallBaseLifecycleMethodAnalyzer.cs @@ -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."); /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(_rule); + public override ImmutableArray SupportedDiagnostics => [_rule]; /// public override void Initialize(AnalysisContext context) diff --git a/Source/CodeAnalysis/Analyzers/FactMethodShouldNamingAnalyzer.cs b/Source/CodeAnalysis/Analyzers/FactMethodShouldNamingAnalyzer.cs index 7f2db71..25749da 100644 --- a/Source/CodeAnalysis/Analyzers/FactMethodShouldNamingAnalyzer.cs +++ b/Source/CodeAnalysis/Analyzers/FactMethodShouldNamingAnalyzer.cs @@ -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."); /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(_rule); + public override ImmutableArray SupportedDiagnostics => [_rule]; /// public override void Initialize(AnalysisContext context) diff --git a/Source/CodeAnalysis/Analyzers/FactOnGivenBaseClassAnalyzer.cs b/Source/CodeAnalysis/Analyzers/FactOnGivenBaseClassAnalyzer.cs index 3bac230..2658ec7 100644 --- a/Source/CodeAnalysis/Analyzers/FactOnGivenBaseClassAnalyzer.cs +++ b/Source/CodeAnalysis/Analyzers/FactOnGivenBaseClassAnalyzer.cs @@ -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."); /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(_rule); + public override ImmutableArray SupportedDiagnostics => [_rule]; /// public override void Initialize(AnalysisContext context) diff --git a/Source/CodeAnalysis/Analyzers/OneSpecificationPerFileAnalyzer.cs b/Source/CodeAnalysis/Analyzers/OneSpecificationPerFileAnalyzer.cs index 26b183e..3e67800 100644 --- a/Source/CodeAnalysis/Analyzers/OneSpecificationPerFileAnalyzer.cs +++ b/Source/CodeAnalysis/Analyzers/OneSpecificationPerFileAnalyzer.cs @@ -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."); /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(_rule); + public override ImmutableArray SupportedDiagnostics => [_rule]; /// public override void Initialize(AnalysisContext context) diff --git a/Source/CodeAnalysis/Analyzers/ShouldMethodMustBeTestMethodAnalyzer.cs b/Source/CodeAnalysis/Analyzers/ShouldMethodMustBeTestMethodAnalyzer.cs index 5f18f86..d27c7e3 100644 --- a/Source/CodeAnalysis/Analyzers/ShouldMethodMustBeTestMethodAnalyzer.cs +++ b/Source/CodeAnalysis/Analyzers/ShouldMethodMustBeTestMethodAnalyzer.cs @@ -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."); /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(_rule); + public override ImmutableArray SupportedDiagnostics => [_rule]; /// public override void Initialize(AnalysisContext context) diff --git a/Source/CodeAnalysis/Analyzers/SpecificationMustBePublicAnalyzer.cs b/Source/CodeAnalysis/Analyzers/SpecificationMustBePublicAnalyzer.cs index 3539596..b18a15b 100644 --- a/Source/CodeAnalysis/Analyzers/SpecificationMustBePublicAnalyzer.cs +++ b/Source/CodeAnalysis/Analyzers/SpecificationMustBePublicAnalyzer.cs @@ -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."); /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(_rule); + public override ImmutableArray SupportedDiagnostics => [_rule]; /// public override void Initialize(AnalysisContext context) diff --git a/Source/CodeAnalysis/SpecificationFacts.cs b/Source/CodeAnalysis/SpecificationFacts.cs index 0fec58b..c472356 100644 --- a/Source/CodeAnalysis/SpecificationFacts.cs +++ b/Source/CodeAnalysis/SpecificationFacts.cs @@ -36,7 +36,7 @@ public static class SpecificationFacts /// /// The convention-discovered lifecycle method names. /// - public static readonly ImmutableArray LifecycleMethodNames = ImmutableArray.Create("Establish", "Because", "Destroy"); + public static readonly ImmutableArray LifecycleMethodNames = ["Establish", "Because", "Destroy"]; static readonly HashSet _testMethodAttributeNames = new(System.StringComparer.Ordinal) { From c44200f82c612a83554c6f5a15adadb86e9cfa66 Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 28 Aug 2026 08:19:42 +0200 Subject: [PATCH 4/4] Align the Cratis ecosystem section with the canonical block --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c18789..6ab8ba2 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ This project is part of [Cratis](https://www.cratis.io) — free, MIT-licensed t - **[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) +- **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.