Compze.Utilities.Testing.XUnit 0.1.0-alpha.3

This is a prerelease version of Compze.Utilities.Testing.XUnit.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Compze.Utilities.Testing.XUnit --version 0.1.0-alpha.3
                    
NuGet\Install-Package Compze.Utilities.Testing.XUnit -Version 0.1.0-alpha.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Compze.Utilities.Testing.XUnit" Version="0.1.0-alpha.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Compze.Utilities.Testing.XUnit" Version="0.1.0-alpha.3" />
                    
Directory.Packages.props
<PackageReference Include="Compze.Utilities.Testing.XUnit" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Compze.Utilities.Testing.XUnit --version 0.1.0-alpha.3
                    
#r "nuget: Compze.Utilities.Testing.XUnit, 0.1.0-alpha.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Compze.Utilities.Testing.XUnit@0.1.0-alpha.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Compze.Utilities.Testing.XUnit&version=0.1.0-alpha.3&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Compze.Utilities.Testing.XUnit&version=0.1.0-alpha.3&prerelease
                    
Install as a Cake Tool

Compze.Utilities.Testing.XUnit

BDD-style specification testing for xUnit v3 — write nested, inheritable test classes that read like executable specifications.

The problem

xUnit runs every [Fact] it finds on a class, including inherited ones. So if you nest test classes and use inheritance to build up context (the way BDD specifications work), every test from every ancestor re-runs on every descendant. A 3-level deep spec tree doesn't just duplicate work — it causes an explosion of redundant test executions.

This makes idiomatic BDD-style testing in plain xUnit impractical.

The solution

This package provides [ExclusiveFact] (and its short alias [XF]) — a custom xUnit v3 fact attribute that runs a test only for the class that declares it, never for inheriting classes. This single mechanism unlocks clean, nested, context-building BDD specifications without any duplicated runs.

What does BDD-style testing look like?

Specifications are organized as nested classes where each level adds context. Class names describe the scenario, test method names describe the expected behavior:

using Compze.Utilities.Testing.XUnit.BDD;

public class When_a_user_attempts_to_register
{
   readonly RegistrationService _service = new();

   public class with_invalid_email : When_a_user_attempts_to_register
   {
      public class that_is_missing_the_at_sign : with_invalid_email
      {
         readonly RegistrationResult _result;
         public that_is_missing_the_at_sign() => _result = _service.Register("johndoe.com", "Secret123!");

         [XF] public void registration_is_rejected()  => Assert.False(_result.Succeeded);
         [XF] public void error_mentions_email()       => Assert.Contains("email", _result.Error, StringComparison.OrdinalIgnoreCase);
      }

      public class that_is_empty : with_invalid_email
      {
         readonly RegistrationResult _result;
         public that_is_empty() => _result = _service.Register("", "Secret123!");

         [XF] public void registration_is_rejected()  => Assert.False(_result.Succeeded);
         [XF] public void error_mentions_required()    => Assert.Contains("required", _result.Error, StringComparison.OrdinalIgnoreCase);
      }
   }

   public class with_invalid_password : When_a_user_attempts_to_register
   {
      public class that_is_too_short : with_invalid_password
      {
         readonly RegistrationResult _result;
         public that_is_too_short() => _result = _service.Register("john@doe.com", "Ab1!");

         [XF] public void registration_is_rejected()     => Assert.False(_result.Succeeded);
         [XF] public void error_mentions_password_length() => Assert.Contains("at least 8", _result.Error);
      }

      public class that_has_no_digit : with_invalid_password
      {
         readonly RegistrationResult _result;
         public that_has_no_digit() => _result = _service.Register("john@doe.com", "SecretPassword!");

         [XF] public void registration_is_rejected()  => Assert.False(_result.Succeeded);
         [XF] public void error_mentions_digit()       => Assert.Contains("digit", _result.Error, StringComparison.OrdinalIgnoreCase);
      }
   }

   public class with_all_valid_data : When_a_user_attempts_to_register
   {
      readonly RegistrationResult _result;
      public with_all_valid_data() => _result = _service.Register("john@doe.com", "Secret123!");

      [XF] public void registration_succeeds()       => Assert.True(_result.Succeeded);
      [XF] public void a_confirmation_email_is_sent() => Assert.True(_result.ConfirmationEmailSent);
      [XF] public void the_user_id_is_assigned()      => Assert.NotEqual(Guid.Empty, _result.UserId);
   }
}

In the Test Explorer this produces a readable specification tree:

When_a_user_attempts_to_register
├── with_invalid_email
│   ├── that_is_missing_the_at_sign
│   │   ├── registration_is_rejected
│   │   └── error_mentions_email
│   └── that_is_empty
│       ├── registration_is_rejected
│       └── error_mentions_required
├── with_invalid_password
│   ├── that_is_too_short
│   │   ├── registration_is_rejected
│   │   └── error_mentions_password_length
│   └── that_has_no_digit
│       ├── registration_is_rejected
│       └── error_mentions_digit
└── with_all_valid_data
    ├── registration_succeeds
    ├── a_confirmation_email_is_sent
    └── the_user_id_is_assigned

Each [XF] test runs exactly once — in the class that declares it — even though child classes inherit parent members.

How context flows through inheritance

The key technique: each nested class inherits from its parent, gaining access to the shared setup (here, _service). Each level's constructor adds its own specific context — the "act" step in each scenario. Tests declared at that level assert the outcome. Because [XF] skips inherited tests, only the assertions declared in each class execute there.

How it works

[ExclusiveFact] / [XF] is an xUnit v3 [Fact] with a custom discoverer. During discovery it compares the declaring type of the test method against the current test class. If they differ (i.e. the test was inherited), the test case is simply not emitted. No reflection hacks, no runtime skipping — just clean xUnit extensibility.

Key benefits

  • Reads like a specification — class names are the context ("Given…", "When…", "And…"), method names are the assertions
  • No duplicated runs — inherited tests are silently excluded at discovery time
  • Shared setup via constructors and inheritance — each nested class adds context on top of the parent, just like BDD context blocks
  • Works with standard xUnit tooling — Test Explorer, dotnet test, CI — everything sees a clean hierarchy
  • Zero ceremony — swap [Fact] for [XF], nest your classes, done
  • xUnit v3 native — built on the v3 extensibility APIs (IXunitTestCaseDiscoverer)

Installation

dotnet add package Compze.Utilities.Testing.XUnit
Package Description
Compze.Utilities.Testing.Must Fluent assertions (Must().Be(), Must().Throw<>(), etc.)
Compze.Tessaging.Hosting.Testing Full integration testing infrastructure
Compze.Utilities.Testing.DbPool Database pool management for tests

License

Apache-2.0

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0-alpha.1 52 2/28/2026
0.1.0-alpha.3 235 2/13/2026
0.1.0-alpha.2 136 2/12/2026
0.1.0-alpha.1 49 2/12/2026