XspecT 19.1.4
dotnet add package XspecT --version 19.1.4
NuGet\Install-Package XspecT -Version 19.1.4
<PackageReference Include="XspecT" Version="19.1.4" />
<PackageVersion Include="XspecT" Version="19.1.4" />
<PackageReference Include="XspecT" />
paket add XspecT --version 19.1.4
#r "nuget: XspecT, 19.1.4"
#:package XspecT@19.1.4
#addin nuget:?package=XspecT&version=19.1.4
#tool nuget:?package=XspecT&version=19.1.4
XspecT — Fluent, specification-style unit testing for .NET
XspecT is a fluent, specification-oriented testing framework for .NET that builds on xUnit. It follows the Given–When–Then pattern and integrates seamlessly with Moq, AutoMock, and AutoFixture. Tests run on the standard xUnit runner and can live side by side with existing xUnit tests.
Whether you are new to unit testing or an experienced practitioner, XspecT helps you express test intent clearly by removing boilerplate, enforcing structure, and generating readable failure descriptions.
Example: testing the PlaceOrder method on ShoppingService:
public class WhenPlaceOrder : Spec<ShoppingService>
{
static Tag<Guid> cartId = new(); // reference an auto-generated Guid
public WhenPlaceOrder()
=> When(_ => _.PlaceOrder(The(cartId)))
.Given<ICartRepository>()
.That(_ => _.GetCart(The(cartId)))
.Returns(A<Cart>());
[Fact] public void ThenCreatesOrder()
=> Then<IOrderService>(_ => _.CreateOrder(The<Cart>()));
}
The example above highlights how XspecT reduces boilerplate by handling test data, dependency mocking, and interaction verification declaratively. In real-world usage, this typically leads to substantially smaller tests compared to xUnit + Moq, while improving readability.
Table of Contents
1. Introduction
To write a test with XspecT, start by subclassing Spec.
Each test is expressed as a specification and executed as a pipeline consisting of three phases:
arrange, act, and assert.
The following is a complete XspecT test class (a specification) containing a single test method (a requirement):
using XspecT;
using XspecT.Assert;
using static App.Calculator;
namespace App.Test;
public class CalculatorSpec : Spec<int>
{
[Fact] public void WhenAdd_1_and_2_ThenSumIs_3() => When(_ => Add(1, 2)).Then().Result.Is(3);
}
1.1 Arrange
The arrange stage defines the setup of the test pipeline.
In XspecT, this is done by calling methods on Spec, either directly or fluently chained together.
The following methods are used to arrange a test:
Given— defines test setup and input dataAfter— setup that runs before the actionBefore— teardown or verification that runs after the action
Although the names After and Before may appear inverted at first glance, the naming reflects their position relative to the When stage, allowing specifications to read fluently as:
When executing the action after setup and before teardown, then this happens.
XspecT also provides mechanisms for preparing and referring to test data in a stable way, so the same values can be consistently reused across arrangement, execution, and assertion.
1.2 Act
The act stage specifies the behavior under test by calling When with a lambda expression.
The lambda takes the subject under test as an argument and should execute the behavior under test.
The subject under test is automatically created based on the arrangement, unless it is static or explicitly provided.
As with arrangement, the order in which Given, After, Before, and When
are declared does not matter. Because execution is deferred until assertion, XspecT can deterministically reorder the pipeline before running it.
So the execution order of the steps is always: Given → After → When → Before.
Each specification defines exactly one action under test and therefore contains a single When stage.
1.3 Assert
XspecT includes a fluent assertion library, XspecT.Assert, conceptually similar to FluentAssertions,
but with a more compact syntax based on the verbs Is, Has, and Does.
The assert stage is specified by calling Then or Result, followed by one or more assertions.
It is only when one of these methods is called that the test pipeline is executed and the result evaluated.
If a test fails, this is either due to an invalid test setup or because an assertion was not satisfied. In the latter case, XspecT provides detailed assertion failures together with an automatically generated description of the specification, making it easier to understand the intended behavior.
Example:
Specification:
=> When(_ => _.List())
.Given<IMyRepository>()
.That(_ => _.List()).Returns(A<MyModel[]>)
.Given().Three<MyModel>()
.Then().Result.Has().Count(4)
Output:
Expected Result to have count 4 but found 3...
----
Given three MyModel
and IMyRepository.List() returns a MyModel[]
When _.List()
Then Result has count 4
In addition to verifying return values, exceptions can also be asserted using Then().Throws.
Given some prior familiarity with NuGet, unit tests and mocking, you should now be ready to start writing your own tests using Spec. For professional use, the remainder of this README serves as a complete, practical guide to structuring specifications, managing test data, and verifying behavior with XspecT.
2. The Test Pipeline
A core feature of XspecT is deferred execution and lazy evaluation: no production code is executed until the first assertion is made. A test runs through four conceptual stages: preparation, execution, assertion, and teardown.
2.1 Preparation
Before the first assertion, the pipeline is configured with test data, mocks, and lambdas to execute.
2.1.1 Creating the Pipeline
You typically create the pipeline by subclassing Spec with two generic arguments. The first is the type of the subject under test and the second is the return type of the method under test.
Other overloads exist for cases without a subject under test or return value.
You can choose to pass a class fixture (a feature of xUnit) to the constructor or start with an empty test pipeline.
2.1.2 Preparing the Pipeline
The preparation steps are recorded and later applied in the following order:
- Default, constraints, and test data are applied in reverse order of declaration
- Mocked behaviour in the order of declaration
2.1.3 Creating the Subject Under Test
After preparation, the pipeline will use AutoMock to create a new instance of your subject under test (unless you provided a value of that type explicitly). If you haven't mocked a certain interface or method that the subject uses, a default mock will be auto-generated.
2.2 Execution
Execution is triggered by the first assertion (technically when Result is referenced or Then() is called).
The pipeline then executes and captures the outcome of the execution.
2.2.1 Running Setup
Setup steps can be provided as lambdas that take the subject under test as arguments, with After().
Setup will be executed in the reverse order of declaration, right after creating the subject under test.
Example:
When(A).After(B).After(C). will result in the execution order: C → B → A.
2.2.2 Executing the Behavior Under Test
The lambda provided with When() will be executed right after setup.
2.2.3 Collecting the outcome
The outcome of a pipeline execution is either a return value or a thrown exception.
If a value is returned, it must match the declared return type and is exposed for assertion through the Result property.
If an exception is thrown, it becomes the captured outcome and can be asserted using the Then().Throws overloads.
Accessing Result will implicitly execute the pipeline if it has not already been executed.
2.3 Assertion
Assertions consume the captured outcome or utilize the mocking framework for verifying execution paths.
The pipeline executes at most once per test method, regardless of the number of references to Result or Then().
Assertions are covered in depth in Chapters 5 and 6.
2.4 Teardown
Teardown steps can be provided as lambdas that take subject under test as arguments, with Before().
Teardown will be executed in the order of declaration when the test class and pipeline are disposed, after the test method has been executed.
Example:
When(A).Before(B).Before(C) will result in the execution order: A → B → C.
2.5 Sync vs. Async Execution
XspecT supports testing synchronous and asynchronous code using the same test pipeline.
When the behavior under test is asynchronous (returns Task or Task<T>), XspecT waits for completion and captures the outcome in the same way as for synchronous code.
The only difference is the lambda signature provided to When, Before, After, and mock setup methods.
Test methods themselves do not need to be async.
As a result, tests for async code read and behave the same way as tests for synchronous code.
3. Using Test Data
XspecT provides helpers for referring to test data that can either be supplied explicitly or automatically generated (optionally with constraints).
Two complementary mechanisms are provided:
- Mentions, for quickly referring to generated values by position or quantity
- Tags, for assigning stable, meaningful identities to values of the same type
3.1. Mentions
Mentions are helper methods for generating and referring to up to five numbered values of a given type, as well as collections of up to five elements. Mentions are resolved per type and per test and always refer to the same value within a specification.
Single values
For a single generated value:
A, An, The, AFirst, TheFirst
For additional values of the same type:
ASecond, TheSecond
AThird, TheThird
AFourth, TheFourth
AFifth, TheFifth
Collections
For collections of generated values:
Zero, One, Two, Three, Four, Five
Some (at least one), Many (at least two), AnyNumberOf
Unreferenced values
For auto-generated values that are not intended to be referenced again:
Any, Another
Ensuring uniqueness
To guarantee that different mentions of the same type resolve to distinct values, the Unique<T> helper may be used.
3.2 Tags
Tags complement mentions by allowing values to be referred to by name rather than position. They are primarily useful when working with multiple values of the same type.
A tag is an instance of Tag<TValue>. Each tag represents exactly one value of the given type.
Example:
protected static Tag<string> name = new(nameof(name));
protected static Tag<int> age = new(nameof(age)), shoeSize = new(nameof(shoeSize));
Providing a name for the tag improves diagnostic output.
The parameterless constructor new() may also be used.
3.2.1 Set and reference tagged values
Tags can be used to set or reference values during pipeline configuration and execution.
Example:
protected static Tag<string> surname = new(), lastname = new();
...
=> Given(surname).Is("Ada").And(lastname).Is("Lovelace")
.When(_ => _.CreateUser(The(surname), The(lastname)))
.Then().Result.FullName.Is("Ada Lovelace");
3.2.2 Use tagged values as default or for auto-generation
Tagged values may also be used as:
- the default value for a given type
- input when auto-generating the subject under test
Example:
Given().Default(name).and.Using(age);
4. Mocking & Auto-Mocking
This part assumes familiarity with Moq or similar mocking frameworks. You should have a clear understanding of when and how mocking is typically used together with xUnit. Here we will examine how the mocking experience can be simplified with the help of XspecT.
4.1 Auto-Mocking subject under test
The subject under test will be created automatically with mocks and default values by AutoMock. Remember from chapter 2 that all mocks are configured after test data has been generated. So regardless of where you provide test data or constraints on test-data, they will be available in the mocking stage of the pipeline execution.
You can supply or modify your own constructor arguments by calling Given or Given().Using.
You can even provide the subject under test by using any of those two methods:
Given(new MyClass(42, "Thursday"))
4.2 Mocking
To mock the behavior of a dependency, call Given<[TheService]>().That(_ => _.[TheMethod](...)).Returns/Throws(...).
That accepts any lambda you would normally supply to the constructor when creating a mock using Moq.
You do not need to create and manage mocks manually, but can supply mocked behaviour directly to the pipeline.
This allows most mocking scenarios to be expressed inline, close to the behavior under test.
4.3 Mocking with arguments
If you want to vary mocked behavior based on arguments, supply a lambda with arguments to Returns. The lambda signature must match the mocked call. Up to five arguments are possible to mock in this way.
Example with two arguments:
=> Given<IMyCalculator>()
.That(_ => _.Add(TheFirst<int>(), TheSecond<int>()))
.Returns((a, b) => a + b) //The mock adds the two arguments passed to the function and returns the sum
4.4 Mocking sequence of calls
Sometimes you want to mock a scenario where more than one call is made to a mock in succession, and the mock should potentially behave differently at each successive call.
You can describe this scenario using the methods First and AndNext.
Example mocking three successive calls:
=> Given<IMyService>().That(_ => _.GetValueAsync())
.First().Returns(() => 1) // returns 1 on first call
.AndNext().Throws(An<ArgumentException>) //throw exception on second call
.AndNext().Returns(); //return on third call
4.5 Observing calls with Tap
Tap allows observing arguments passed to a mocked call without affecting its behavior. A method with up to five arguments is possible to tap.
Example:
int _tappedValue = -1;
=> Given<IMyInterface>()
.That(_ => _.Get(An<int>()))
.Tap<int>(i => _tappedValue = i)
.Returns(() => _retVal)
4.6 Verification
To verify a call to a mocked dependency, call Then<[TheService]>([SomeLambdaExpression]).
Both mocking and verification of behavior are based on Moq framework.
Example:
namespace MyProject.Spec.ShoppingService;
public class WhenPlaceOrder : Spec<MyProject.ShoppingService>
{
protected WhenPlaceOrder()
=> When(_ => _.PlaceOrder(An<int>()))
.Given<ICartRepository>().That(_ => _.GetCart(The<int>()))
.Returns(() => A<Cart>(_ => _.Id = The<int>()));
[Fact] public void ThenOrderIsCreated() => Then<IOrderService>(_ => _.CreateOrder(The<Cart>()));
[Fact] public void ThenLogsOrderCreated()
=> Then<ILogger>(_ => _.Information($"OrderCreated from Cart {The<int>()}"));
}
The built-in mocking capabilities of XspecT cover almost all scenarios that Moq covers.
But should you need some feature that is not provided by XspecT, you can create your mock using Moq explicitly and supply it to the pipeline using Given(myMock.Object).
5. Asserting Results
XspecT comes with its own fluent assertion framework under the XspecT.Assert namespace.
Even if you don't use any other feature of XspecT, you can use this framework as an alternative to FluentAssertions or AwesomeAssertions.
However, combined with the XspecT pipeline is where it really shines.
Reading advice What follows is a compact reference manual on the fluent nature of XspecT.Assert, followed by a complete list of features.
5.1 Fluent assertions
Assertions are made directly on the value to be verified. Every assertion returns a continuation, allowing chaining of assertions. The continuation is context-aware and allows different assertions depending on what was asserted previously.
5.1.1 And
When you want to combine more than one assertion, all of which must pass
3.Is().GreaterThan(2).and.LessThan(4);
5.1.2 Either - Or
When you want to combine two assertions, one of which must pass
3.Is().either.GreaterThan(4).or.LessThan(4);
5.1.3 Not
Any assertion can be negated by placing not before (note lowercase not)
3.Is().not.GreaterThan(4);
5.2 Values
Values of any type can be verified with any of the two extension methods Is and Has
5.2.1 Is
- Equal:
Result.Is(3)
Result.Is().EqualTo(3) - Equivalent: (for objects)
Result.Is().Like(new MyObject {Id = 3})
Result.Is().EquivalentTo(new MyObject {Id = 3}) - Not equal:
Result.Is().Not(3) - Null:
Result.Is().Null() - Greater than:
3.Is().GreaterThan(2) - Less than:
3.Is().LessThan(2) - Aproximately equal with tolerance:
Result.Is().Around(3, 0.1) - Even: (true if number is divisible by 2)
Result.Is().Even() - OneOf:
Result.Is().OneOf(Three<int>()) - True: (for booleans)
Result.Is().True() - False: (for booleans)
Result.Is().False()
5.2.2 Has
- Verify that the result has a given condition:
Result.Has(_ => _.Id == 3) - Verify that the result has the given type:
Result.Has().Type<MyModel>()
5.3 Strings
5.3.1 Is
- Like
" ABC ".Is().Like("abc") - EquivalentTo
" ABC ".Is().EquivalentTo("abc") - Empty
"".Is().Empty() - NullOrEmpty
((string)null).Is().NullOrEmpty() - NullOrWhitespace
" ".Is().NullOrWhitespace()
5.3.2 Does
- Contain
"ABC".Does().Contain("AB") - StartWith
"ABC".Does().StartWith("AB") - EndWith
"ABC".Does().EndWith("BC")
5.4 Time
- Before
DateTime.Now.Is().Before(DateTime.Now.AddDays(1)) - After
DateTime.Now.Is().After(DateTime.Now.AddDays(-1)) - CloseTo
DateTime.Now.Is().CloseTo(DateTime.Now.AddDays(1), TimeSpan.FromDays(2))
TimeSpan.FromDays(4).Is().CloseTo(TimeSpan.FromDays(3), TimeSpan.FromDays(2)) - Positive
TimeSpan.FromDays(1).Is().Positive() - Negative
TimeSpan.FromDays(1).Is().Negative()
5.5 Collections
5.5.1 Is
- EqualTo
all elements are equal and in the same order
list.Is().EqualTo(otherList) - Like
all elements are equal but order may differ
list.Is().Like(otherList) - SameAs
reference equal
list.Is().SameAs(otherList) - EquivalentTo
all elements are equal but order may differ
list.Is().EquivalentTo(otherList) - Empty
list.Is().Empty() - Distinct
list.Is().Distinct()// all elements in the collection are different
list.Is().Distinct(it => it.Id)// all elements have different values of the given property
5.5.2 Does
- Contain
list.Does().Contain(3)
5.5.3 Has
- Count
list.Has().Count(3)
list.Has().Count(it => it > 3).At(2)// with condition - Count at least
list.Has().Count().AtLeast(2)
list.Has().Count(it => it > 3).AtLeast(2)// with condition - Count at most
list.Has().Count().AtMost(2)
list.Has().Count(it => it > 3).AtMost(2)// with condition - Count in range
list.Has().Count().InRange(2, 4)
list.Has().Count(it => it > 3).InRange(2, 4)// with condition - Order ascending
list.Has().Order().Ascending()
list.Has().Order(it => it.Age).Ascending()// with condition - Order descending
list.Has().Order().Descending()
list.Has().Order(it => it.Age).Descending()// with condition - [One/Two/Three/Four/Five]Items
verify that the collection has the given number of items and return them as a n-tuple
number.Has().OneItem().that.Is(3)// numbers have one item, and that item is 3
patients.Has().OneItem().that.Age.Is(3)// patients have one item, and its age is 3
patients.Has().OneItem(it => it.Age == 3).that.Gender.Is('F')// patients have one item aged 3, and its gender is female - All
list.Has().All(it => it.Age > 3)// all items in the collection match the criteria
list.Has().All((it, i) => it.Age > i)// with index of item
list.Has().All(it => it.Age.Is().GreaterThan(3))// apply assertion to all items - Some
list.Has().Some(it => it.Age > 3)- at least one item in the collection matches the criteria - None
list.Has().None(it => it.Age > 3)- no item in the collection matches the criteria
6. Guidelines
6.1 Recommended test structure
Based on the way xUnit and XspecT work and on experience using it, this is an opinionated recommendation on how to structure your tests using XspecT. The goal of these conventions is to keep specifications readable, navigable, and aligned with production structure as test suites grow.
- Mimic the folder structure of your production code to be tested
- Create one project per production project called [ProductionFolder].Test or [ProductionFolder].Spec
- Create a leaf-folder per subject under test called [NameOfClass]
- Create one test-class per method under test, called WHEN[NameOfMethod]
- Let the class for each method under test be abstract and nest concrete classes inside it for each different setup, called Given[SomePrecondition]
- Place setup in the constructor and assertions in the test methods
- Feel free to nest given classes in more than one layer (but avoid more than four levels of nesting)
- Write one test-method per logical assertion (i.e. test only one thing per test)
- Feel free to use all of Xunit's features - such as Fact, Theory and test-data
- Use only the built-in assertion framework from XspecT (it will give you cleaner specifications with clearer test-output)
Example:
public abstract WhenPlaceOrder : Spec<ShoppingService>
{
static Tag<Guid> cartId = new();
protected WhenPlaceOrder() => When(_ => _.PlaceOrder(The(cartId)));
public abstract GivenCartExists : WhenPlaceOrder
{
protected GivenCartExists
=> Given<ICartRepository>().That(_ => _.GetCart(The(cartId))).Returns(A<Cart>());
public WithItems : GivenCartExists
{
...
}
public WithoutItems : GivenCartExists
{
...
}
}
public GivenCartNotExists : WhenPlaceOrder
{
public GivenCartNotExists
=> Given<ICartRepository>().That(_ => _.GetCart(The(cartId))).Returns(() => Cart.NoCart);
...
}
}
6.2 Class fixtures
XspecT supports the xUnit feature of sharing setup between tests in a common class fixture.
A class fixture is created in the same way as a test class, by inheriting Spec and providing setup.
The only difference between a class fixture implemented with XspecT and a test class implemented with XspecT
is that class fixtures don't have test methods or assertions.
When using a class fixture and having more than one test method, no setup should be put in the constructor, since the constructor is run once for each test method and provides the setup to the shared class fixture (i.e. would add the same setup multiple times, including after the test pipeline was executed, which XspecT does not allow).
6.3. Some final advice
Unit tests work best when they run fast. Write modular production code in line with best practices, so that each unit can be tested in isolation while mocking or ignoring the rest. This enables tiny test methods with a single logical assertion and shared setup.
However, remember that the entire test pipeline is built and disposed for each test method (a feature of xUnit). If a specification requires non-trivial setup or execution time, it can be reasonable to group multiple closely related assertions into the same test method to reduce overall suite runtime.
XspecT is designed to thrive in clean, well-structured codebases. Its emphasis on explicit structure and readable specifications is intended to reinforce those qualities, helping teams maintain clarity and confidence as both code and test suites grow.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- AutoFixture (>= 5.0.0-preview0012)
- Moq (>= 4.20.72)
- Moq.AutoMock (>= 3.6.1)
- xunit (>= 2.9.3)
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 |
|---|---|---|
| 19.1.4 | 259 | 2/3/2026 |
| 19.1.3 | 96 | 1/31/2026 |
| 19.1.2 | 106 | 1/22/2026 |
| 19.1.1 | 98 | 1/21/2026 |
| 19.1.0 | 104 | 1/3/2026 |
| 19.0.1 | 111 | 1/2/2026 |
| 19.0.0 | 107 | 1/2/2026 |
| 18.12.0 | 108 | 1/1/2026 |
| 18.11.0 | 292 | 12/17/2025 |
| 18.10.1 | 349 | 12/6/2025 |
| 18.10.0 | 198 | 12/5/2025 |
| 18.9.4 | 233 | 10/15/2025 |
| 18.9.3 | 220 | 10/14/2025 |
| 18.9.2 | 176 | 10/4/2025 |
| 18.9.1 | 239 | 9/20/2025 |
| 18.8.3 | 196 | 9/11/2025 |
| 18.8.2 | 595 | 8/9/2025 |
| 18.8.1 | 328 | 8/6/2025 |
| 18.8.0 | 673 | 7/23/2025 |
Add constraint/transform as default to any type in a continuation with `Given` or `And`,
ex: `Given'int'(i => i > 0 ? i : -i).And'byte'(b => b % 2 == 0 ? b : 2 * b)`
Added assert Uppercase and Lowercase to `IsString`, ex: `The'string'().Is().Uppercase()`