xchain 0.4.0
dotnet add package xchain --version 0.4.0
NuGet\Install-Package xchain -Version 0.4.0
<PackageReference Include="xchain" Version="0.4.0" />
<PackageVersion Include="xchain" Version="0.4.0" />
<PackageReference Include="xchain" />
paket add xchain --version 0.4.0
#r "nuget: xchain, 0.4.0"
#:package xchain@0.4.0
#addin nuget:?package=xchain&version=0.4.0
#tool nuget:?package=xchain&version=0.4.0
XChain
XChain extends xUnit with structured test chaining, shared context between steps, and smart skipping based on previous failures. It's ideal for integration tests, API flows, or any scenario where test steps depend on each other and need to run in a defined order.
Features
- Chain test steps using
[ChainFact]
or[ChainTheory]
with readable display names. - Share structured state via
TestChainOutput
. - Automatically skip downstream tests when failures occur.
- Chain multiple test collections, synchronizing execution with full isolation.
- Filter test runs using custom traits with strongly-typed metadata.
- Compatible with async workflows and supports rich diagnostics.
Why XChain?
Traditional xUnit tests are isolated by design — great for unit tests, but limiting for integration or scenario tests where later steps depend on earlier ones.
XChain solves this by enabling:
- Logical chaining within a class (
Link
-ordered[ChainFact]
) - Shared, strongly-typed test output values
- Explicit collection orchestration
- Exception tracking and conditional skipping
- Filtering with trait-based metadata
How It Works
XChain uses xUnit’s extensibility to provide:
- Custom
[ChainFact]
/[ChainTheory]
attributes that inject ordering, naming, and flow grouping. - A fixture-based pattern (
TestChainContextFixture
) for output sharing and exception flow. - Fluent APIs like
.Link
,.LinkUnless
,.LinkWithCollection
, etc. to capture and control flow. - Collection coordination using dedicated
Setup
andAwait
fixtures. - Strong typing for dictionary-style output sharing.
Quick Start
[TestCaseOrderer("Xchain.TestChainOrderer", "Xchain")]
public class SimpleTestChain(TestChainContextFixture chain) : IClassFixture<TestChainContextFixture>
{
[ChainFact(Link = 1, Name = "Prepare Data")]
public void Step1() =>
chain.Link(output => output["Token"] = "abc123");
[ChainFact(Link = 2, Name = "Use Data")]
public void Step2() =>
chain.Link(output =>
{
var token = output.Get<string>("Token");
Assert.Equal("abc123", token);
});
}
Each [ChainFact]
defines a link in the chain. You can pass values between steps using output
, and if a previous step fails, later steps can be automatically skipped.
Key Concepts
XChain builds upon xUnit's extensibility to introduce a minimal and powerful abstraction for test chaining. Here are the key features and how they work:
ChainFact
and ChainTheory
These are enhanced versions of [Fact]
and [Theory]
that introduce:
- Link-based ordering: Test execution is driven by an integer
Link
, which determines order within the test class. - Flow grouping: An optional
Flow
label allows grouping and visualizing test steps in Test Explorer. - Named steps: The
Name
property contributes to human-readable, sortable test display names.
[ChainFact(Link = 10, Name = "Login", Flow = "User Flow")]
public void LoginTest() => ...
The display name will appear as:
#10 | User Flow | Login
This improves traceability in test logs and IDE explorers.
Linking Logic (Link
, LinkUnless
, LinkAsync
, etc.)
XChain provides fluent methods to structure test execution:
Link
: Run test logic and track exceptions.LinkUnless<TException>
: Skip test if a specific exception already occurred.LinkAsync
: Asynchronous variant with timeout support.SkipIf<TException>
: Explicitly skip a test if a condition exists in error history.
Each method records failures into a structured error stack (TestChainErrors
), and subsequent steps can respond accordingly.
Shared Context (TestChainOutput
)
A shared, thread-safe dictionary allows test steps to share values:
chain.Link(output => output["UserId"] = 123);
...
chain.Link(output =>
{
var userId = output.Get<int>("UserId");
...
});
You can also build reusable strongly typed accessors using TestOutput<TCollection, TOutput>
to ensure key uniqueness across collections.
Chaining Collections
XChain enables one collection to wait for another to finish using:
CollectionChainLinkSetupFixture<T>
— registers a collection as a chain step.CollectionChainLinkAwaitFixture<T>
— blocks until the registered collection completes.
This is essential for cross-collection orchestration:
public class SetupFixture : CollectionChainLinkSetupFixture<MyCollection>;
public class AwaitFixture : CollectionChainLinkAwaitFixture<MyCollection>;
You can then fluently access shared output using:
chain.LinkWithCollection<PreviousCollection>(..., ...)
Note: Collection timeouts default to 6 minutes. This can be configured in the CollectionChainLinkAwaitFixture
.
Global Metadata via Traits
XChain includes a TraitDiscoverer
that turns strongly-typed attributes into metadata for filtering and organizing tests.
[Metadata("SmokeTests")]
public class MyTestSuite { }
This can be used to group collections and run them by trait.
Optional Collection Orderer
You may also define collection-wide execution order using:
[CollectionDefinition("MyCollection")]
[CollectionChainOrder(1)]
public class OrderedCollection { }
However, this requires global test parallelism to be disabled, so it is generally less recommended than explicit Await
/Setup
chaining.
Each of these features builds on xUnit principles while maintaining full compatibility. You can opt-in incrementally — use as little or as much as your test architecture requires.
Creating Strongly Typed Output Keys
When working with shared outputs across tests or collections, it's helpful to avoid using raw string keys. XChain supports strongly typed access via TestOutput<TCollection, TOutput>
, which encapsulates the key naming and value casting.
Step 1: Define a Typed Extension
Create an extension method to encapsulate a reusable key. Here's an example for a shared Guid
:
public static class TestChainOutputExtensions
{
public static TestOutput<T, Guid> SharedId<T>(this TestChainOutput output) =>
new(output, "SharedId");
}
The T
type ensures key uniqueness by prefixing it with the type name (e.g., SetupTests_SharedId
), making it safe across multiple collections.
Step 2: Store a Value in the Output
In the producing test or collection, use Put
to store a value:
[Collection("SetupCollection")]
public class SetupTests(CollectionChainContextFixture chain)
{
[ChainFact(Link = 1, Name = "Generate Id")]
public void GenerateId() =>
chain.Link(output => output.SharedId<SetupTests>().Put(Guid.NewGuid()));
}
Step 3: Retrieve the Value from Another Collection
In the consuming test, use Get
to retrieve the value:
[Collection("ConsumerCollection")]
public class ConsumerTests(CollectionChainContextFixture chain)
{
[ChainFact(Link = 1, Name = "Read Shared Id")]
public void ReadId() =>
chain.Link(output =>
{
var id = output.SharedId<SetupTests>().Get();
Assert.NotEqual(Guid.Empty, id);
});
}
Benefits
- Type-safe access to output data.
- Fluent syntax for both producers and consumers.
- Unique scoping of keys per test or collection type.
- Simplified refactoring, avoiding scattered string literals.
This pattern helps enforce clarity and consistency when passing values between chained tests.
- Powered by Xunit.SkippableFact
- Created from JandaBox
- Icon by Freepik – Flaticon
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. 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. |
.NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.1 is compatible. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- xunit (>= 2.9.3)
- xunit.abstractions (>= 2.0.3)
- xunit.extensibility.core (>= 2.9.3)
- Xunit.SkippableFact (>= 1.5.23)
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.4.0 | 445 | 6/29/2025 |
0.3.5-try-output.7 | 120 | 6/18/2025 |
0.3.5-try-output.2 | 121 | 6/18/2025 |
0.3.5-main.2 | 74 | 6/29/2025 |
0.3.5-main.1 | 122 | 6/18/2025 |
0.3.5-collection-link.18 | 69 | 6/29/2025 |
0.3.5-collection-link.17 | 71 | 6/29/2025 |
0.3.5-collection-link.12 | 285 | 6/19/2025 |
0.3.5-collection-link.6 | 119 | 6/18/2025 |
0.3.5-collection-link.5 | 117 | 6/18/2025 |
0.3.4 | 146 | 6/18/2025 |
0.3.4-test-output.3 | 118 | 6/18/2025 |
0.3.4-test-output.2 | 119 | 6/18/2025 |
0.3.4-test-output.1 | 121 | 6/18/2025 |
0.3.4-main.1 | 118 | 6/18/2025 |
0.3.3 | 231 | 6/16/2025 |
0.3.3-main.1 | 121 | 6/16/2025 |
0.3.3-chain-linker.7 | 129 | 6/16/2025 |
0.3.2 | 230 | 6/13/2025 |
0.3.2-main.2 | 194 | 6/13/2025 |
0.3.2-flow-fact.3 | 238 | 6/13/2025 |
0.3.2-chain-linker.6 | 263 | 6/11/2025 |
0.3.1 | 308 | 5/26/2025 |
0.3.1-skipped-reason-emoji.1 | 128 | 5/26/2025 |
0.3.1-main.1 | 126 | 5/26/2025 |
0.3.0 | 153 | 5/26/2025 |
0.2.2-main.1 | 128 | 5/26/2025 |
0.2.2-chain-name-and-order.7 | 79 | 5/25/2025 |
0.2.2-chain-name-and-order.5 | 46 | 5/24/2025 |
0.2.1 | 158 | 5/22/2025 |
0.2.1-trait-discoverer.1 | 120 | 5/21/2025 |
0.2.1-main.1 | 122 | 5/22/2025 |
0.2.0 | 165 | 5/19/2025 |
0.1.0 | 162 | 5/18/2025 |
0.1.0-test-chain.3 | 127 | 5/18/2025 |
0.1.0-main.4 | 122 | 5/18/2025 |
0.1.0-main.2 | 128 | 5/18/2025 |
0.1.0-ci.1 | 124 | 5/18/2025 |
0.1.0-chain-orderer.3 | 123 | 5/18/2025 |