Functional.xunit
UnambitiousFx.Functional.xunit adds fluent assertions for Result, Result<T> and Maybe<T> to your xUnit test suites. Instead of unpacking results by hand, you state the branch you expect and chain focused checks onto the value or failure.
Why use it
- Express intent, not plumbing.
result.ShouldBe().Success()reads like the behavior under test, with noTryGetValue/outboilerplate. - Rich failure messages. A failing assertion reports the actual state, including the failure code and message, so a red test tells you what went wrong.
- Typed failures. Narrow a failure to
ValidationFailure,NotFoundFailure,ConflictFailure,BadRequestFailure, or anyIFailureand assert its specific properties. - Sync and async parity. The same
ShouldBe()chain works onTask<Result<T>>andValueTask<Result<T>>(and theirMaybeequivalents) without awaiting first.
Install
- .NET CLI
- PackageReference
dotnet add package UnambitiousFx.Functional.xunit
<PackageReference Include="UnambitiousFx.Functional.xunit" Version="*" />
The ShouldBe() entry point
Every assertion starts from ShouldBe(), then narrows to the expected case:
result.ShouldBe()returns aResultAssertion(orResultAssertion<T>forResult<T>).maybe.ShouldBe()returns aMaybeAssertion<T>.
From there you pick a branch:
| Source | Branch method | Returns |
|---|---|---|
Result | .Success(because?) | SuccessAssertion |
Result<T> | .Success(because?) | SuccessAssertion<T> |
Result/<T> | .Failure(because?) | FailureAssertion |
Maybe<T> | .Some(because?) | SomeAssertion<T> |
Maybe<T> | .None(because?) | NoneAssertion |
Each branch method takes an optional because reason that is woven into the failure message.
Quick example — Result
using UnambitiousFx.Functional;
using UnambitiousFx.Functional.Failures;
using UnambitiousFx.Functional.xunit;
using Xunit;
[Fact]
public void Parse_ValidValue_ReturnsSuccess()
{
// Arrange (Given)
var input = "42";
// Act (When)
var result = Parse(input);
// Assert (Then)
result.ShouldBe()
.Success()
.And(value => Assert.Equal(42, value));
}
[Fact]
public void Parse_InvalidValue_ReturnsValidationFailure()
{
// Arrange (Given)
var input = "abc";
// Act (When)
var result = Parse(input);
// Assert (Then)
result.ShouldBe()
.Failure()
.WhichIsValidationError()
.WithFailureContaining("integer");
}
Quick example — Maybe
using UnambitiousFx.Functional;
using UnambitiousFx.Functional.xunit;
using Xunit;
[Fact]
public void FindUser_ExistingEmail_ReturnsSome()
{
// Arrange (Given)
var email = "john@example.com";
// Act (When)
var maybe = FindUser(email);
// Assert (Then)
maybe.ShouldBe()
.Some()
.And(user => Assert.Equal(email, user.Email));
}
Arrange-Act-Assert convention
Tests in this family follow the Arrange-Act-Assert (AAA) pattern, which maps directly onto Gherkin (Given-When-Then). Use comments to separate the three sections and name tests MethodName_Scenario_ExpectedBehavior. See Test Organization for the full convention.
Explore the assertions
- Result Assertions —
.Success()/.Failure(), chaining, message/code checks, and typed failures. - Maybe Assertions —
.Some()/.None()and value chaining. - Async Assertions — assert directly on
Task<Result<T>>/ValueTask<Result<T>>and asyncMaybe. - Test Organization — AAA/Gherkin, naming, theories, and an edge-case checklist.