aweXpect
aweXpect is a fluent assertion library for .NET. It turns test verifications into readable sentences and produces
failure messages that explain in plain English what was expected and what actually happened.
await Expect.That(message).IsEqualTo("It's a compliment");
Expected that message
is equal to "It's a compliment",
but it was "It's a complement", which differs at index 12:
↓ (actual)
"…a complement"
"…a compliment"
↑ (expected)
Why aweXpect?
- Extensible
- Async-first
- Performant
- Any runner
- Native AOT
The aweXpect.Core package is intentionally kept stable so extensions
don't fight over versions. Ready-made extension packages cover JSON
(aweXpect.Json), HTTP (aweXpect.Web),
architecture tests (aweXpect.Reflection), file and time systems
(aweXpect.Testably) and mocks
(aweXpect.Mockolate).
Add expectations for any other type with extension methods on IThat<TType>:
public static AndOrResult<Track, IThat<Track?>> IsRadioFriendly(
this IThat<Track?> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint((it, grammars)
=> new IsRadioFriendlyConstraint(it, grammars)),
subject);
// usage
await Expect.That(track).IsRadioFriendly();
The full walkthrough is at Extending aweXpect.
Every expectation is awaited. The same fluent chain works for sync values, Task, IAsyncEnumerable, exceptions and
HTTP responses (with the separate aweXpect.Web package): there is no
parallel …Async() API to remember.
// Plain value
await Expect.That(result).IsEqualTo(42);
// Task<T>
await Expect.That(_users.GetAsync(id))
.Satisfies(u => u.IsActive);
// IAsyncEnumerable<T> - the cancellation token flows through the stream
await Expect.That(_orders.StreamAsync())
.Contains(o => o.IsPriority)
.WithCancellation(cancellationToken);
// Delegate that throws
await Expect.That(() => _payments.ChargeAsync(-1m))
.ThrowsExactly<ArgumentOutOfRangeException>();
Because the chain is awaited, Because(...) and WithCancellation(...) attach once at the end instead of being
threaded through every method. Multiple expectations compose directly via Expect.ThatAll(...), without a
thread-static assertion scope.
The happy path (passing assertions, the common case in a healthy test suite) is the path that has been optimised.
Comparing strings, ignoring case
See the benchmarks for the full set.
[Fact] // xUnit
[Test] // NUnit / TUnit
[TestMethod] // MSTest
public async Task IsActive()
=> await Expect.That(user.IsActive).IsTrue();
xUnit (v2 and v3), NUnit (v3, v4 and v5), MSTest and TUnit are detected automatically: a failure usually throws the
assertion exception of the test framework, see
test frameworks for the exact types. Fail and Skip
fail or skip a test without an expectation.
aweXpect and aweXpect.Core are trimmable and AOT compatible for net8.0 and later, so a test project can be
published with Native AOT:
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>
Equivalency and event recording need the members and events of your types. A source generator that ships with the
aweXpect package registers them for each call site when your project is compiled, so they work without reflection. A
comparison or recording of a type the generator could not see fails with an error that names the type and tells you
how to register it, instead of silently verifying less.
The details are in Native AOT and trimming.
Migrating from another assertion library?
The aweXpect.Migration analyzer ships code-fix providers that rewrite
most FluentAssertions and xunit.Assert call sites for you. See the migration guide for the full
walkthrough.
Where to go next
- Getting Started: install the package and write your first expectation.
- Migration: automated code fixes for moving from FluentAssertions or
xunit.Assert. - How aweXpect works: awaiting, negation, combining,
null, options, time and configuration. - Native AOT and trimming: how equivalency and event recording work in a test project published with trimming or Native AOT.
- Extending aweXpect: add expectations for your own types using
aweXpect.Core.