fluentassertions
Reference for FluentAssertions - the canonical .NET fluent-assertion library pairable with xUnit / NUnit / MSTest; provides `.Should()` extension API (`.Should().Be()`, `.Should().BeOfType()`, `.Should().Throw()`, `.Should().BeEquivalentTo()` for deep equality, `.Should().Satisfy()` for predicates, `.Should().BeApproximately()` for floats); rich failure messages with object structure visualization. Covers the v8 license change: v8+ is free for open-source and non-commercial use but requires a paid license for commercial use, while v7 remains fully open-source. Use when a .NET test project needs deep object comparison or better failure output than `Assert.X` gives, when assertions must survive a move between xUnit / NUnit / MSTest, or when picking between v7 and v8+ on license grounds.
Install with skills.sh (any agent)
npx skills add testland/qa --skill fluentassertionsfluentassertions
Overview
Per fluentassertions.com (opens in new window):
FluentAssertions is the de facto fluent-assertion library for .NET. Works with any of xunit-tests, nunit-tests, mstest-tests.
Important license change note: from v8, "commercial use requires a paid license", while v8+ stays "free for open-source projects and non-commercial use"; v7 "will remain fully open-source indefinitely" (per fluentassertions.com/releases (opens in new window)). Commercial projects either buy a v8+ license or pin to v7; open-source and non-commercial projects can use v8+ free.
This skill is a reference - defines the matcher catalog; doesn't run tests. Pair with one of the test frameworks.
When to use
Step 1 - Install
dotnet add package FluentAssertions # current v8+ (see Overview on licensing)
dotnet add package FluentAssertions --version 7.0.0 # pin v7 for fully-OSS commercial useStep 2 - Basic syntax
using FluentAssertions;
result.Should().Be(42);
list.Should().HaveCount(3);
string.Should().StartWith("Hello");
exception.Should().Be<ArgumentNullException>();The .Should() extension method provides the fluent entry-point.
Step 3 - Matchers catalog
Per fluentassertions.com/introduction (opens in new window). Core matchers (full catalog in references/matchers.md):
value.Should().Be(expected); // equality
value.Should().BeNull();
n.Should().BeGreaterThan(0); // numeric
s.Should().StartWith("prefix"); // string
list.Should().HaveCount(3).And.Contain("alice"); // collections
result.Should().BeOfType<Success>(); // type
act.Should().Throw<ArgumentException>().WithMessage("*invalid*"); // exceptionsStep 4 - Combining matchers
.And chains assertions:
list.Should().HaveCount(3).And.Contain("alice").And.NotContain("eve");.Which accesses the result for further assertion:
result.Should().BeOfType<Success>()
.Which.Value.Should().Be(42);Step 5 - Failure messages
Failure output shows the object structure, unlike Assert.AreEqual:
Expected list to have 4 items, but found 3:
["alice", "bob", "charlie"]Step 6 - BeEquivalentTo deep equality
Structural (deep) comparison; the most powerful matcher:
actual.Should().BeEquivalentTo(expected); // deep equal, order-independentCross-type comparison and options (Excluding, Including, ComparingByMembers, WithStrictOrdering, IgnoringCyclicReferences): references/matchers.md.
Step 7 - Migration considerations
For migration FROM:
Migration cost: low (mechanical). Migration benefit: richer failure messages + chainable assertions.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Mix Assert.X and .Should() styles in same suite | Reader confusion | Pick one + lint enforcement |
Long BeEquivalentTo chains without options | Compares fields you don't care about; brittle | Use Excluding to scope (Step 6) |
| Ship v8+ in a commercial project without a paid license | License violation | Buy a v8+ license or pin v7 (Step 1) |
value.Should().Be(true) instead of BeTrue() | Loses semantic clarity | Use BeTrue() (Step 3) |
Skip WithMessage on exception assertions | Pass for wrong exception type | Always specify expected message (Step 3) |
Limitations
References
FluentAssertions matcher catalog
View source (opens in new window)FluentAssertions matcher catalog
Complete .Should() matcher reference. Core examples live in SKILL.md Step 3; this file is the full catalog. Per fluentassertions.com/introduction (opens in new window).
Equality
value.Should().Be(expected);
value.Should().NotBe(expected);
value.Should().BeNull();
value.Should().NotBeNull();
value.Should().BeSameAs(other); // reference equalityNumeric
n.Should().BeGreaterThan(0);
n.Should().BeLessThanOrEqualTo(100);
d.Should().BeApproximately(3.14, 0.01);String
s.Should().StartWith("prefix");
s.Should().EndWith("suffix");
s.Should().Contain("substring");
s.Should().Match("*wildcard*");
s.Should().MatchRegex(@"\d+");
s.Should().NotBeNullOrEmpty();Collections
list.Should().HaveCount(3);
list.Should().Contain("alice");
list.Should().NotContain("eve");
list.Should().ContainInOrder("alice", "bob");
list.Should().BeEquivalentTo(other); // any order
list.Should().AllSatisfy(x => x.Should().BePositive());Type checks
result.Should().BeOfType<Success>();
result.Should().BeAssignableTo<IResult>();Exceptions
Action act = () => DoSomething();
act.Should().Throw<ArgumentException>()
.WithMessage("*invalid*")
.Where(e => e.ParamName == "name");
// Async
Func<Task> asyncAct = async () => await DoSomethingAsync();
await asyncAct.Should().ThrowAsync<HttpRequestException>();
// Should NOT throw
act.Should().NotThrow();Boolean + null
flag.Should().BeTrue();
flag.Should().BeFalse();
opt.Should().BeNull();
opt.Should().NotBeNull().And.NotBeEmpty();Custom predicates
user.Should().Satisfy(u => u.Email.Contains("@") && u.Age >= 18);BeEquivalentTo deep equality
Structural comparison; the most powerful matcher.
var actual = new User { Id = 1, Name = "Alice", Address = new Address { City = "NYC" } };
var expected = new User { Id = 1, Name = "Alice", Address = new Address { City = "NYC" } };
actual.Should().BeEquivalentTo(expected); // passes (deep equal)
// Across different types (record vs class):
var dto = new UserDto { Id = 1, Name = "Alice" };
user.Should().BeEquivalentTo(dto, opts => opts
.Excluding(u => u.PasswordHash)); // ignore field
// With options
actual.Should().BeEquivalentTo(expected, opts => opts
.Excluding(x => x.Timestamp)
.ComparingByMembers<MyType>()
.WithStrictOrdering()
);Options control: Excluding, Including, ComparingByMembers, WithStrictOrdering, WithoutStrictOrdering, IgnoringCyclicReferences.
Related skills
mstest-tests
Configures and runs MSTest (now MSTest.TestFramework v3) - Microsoft's first-party .NET test framework with `[TestClass]` / `[TestMethod]` / `[DataRow]` / `[DynamicData]` attributes; `[ClassInitialize]` / `[ClassCleanup]` / `[TestInitialize]` / `[TestCleanup]` lifecycle; `TestContext` injection; tight Visual Studio + dotnet test integration. Use when working with .NET on a MSTest codebase, or in environments standardized on Microsoft toolchain.
nunit-tests
Configures and runs NUnit - JVM-style attribute-driven .NET test framework with `[Test]` / `[TestCase]` / `[TestCaseSource]` / `[Values]` / `[Random]` parametrize attributes; `[SetUp]` / `[TearDown]` / `[OneTimeSetUp]` / `[OneTimeTearDown]` lifecycle; categories for selective runs; constraint-model assertion API (`Assert.That(actual, Is.EqualTo(expected))`); parameterized fixtures via `[TestFixture]` typed args. Use when working with .NET on a NUnit codebase or preferring constraint-model assertions over xUnit's classic style.
xunit-tests
Configures and runs xUnit.net (xUnit v2 + v3) - current de facto .NET test framework with `[Fact]` for single tests + `[Theory]` + `[InlineData]`/`[ClassData]`/`[MemberData]` for parametrized; collection fixtures (`[Collection]`) + class fixtures (`IClassFixture`) for shared setup; output via `ITestOutputHelper`; parallel test config via assembly attribute. Use when working with .NET (C# / F# / VB.NET) on the modern test stack.