Testland
Browse all skills & agents

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 fluentassertions
View source

fluentassertions

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

  • .NET project using any test framework (xUnit / NUnit / MSTest).
  • Need richer assertion failure messages than the built-in Assert.X methods.
  • Deep-equality checking via BeEquivalentTo.
  • Migrating between test frameworks (assertion code stays the same).

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 use

Step 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*");   // exceptions

Step 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-independent

Cross-type comparison and options (Excluding, Including, ComparingByMembers, WithStrictOrdering, IgnoringCyclicReferences): references/matchers.md.

Step 7 - Migration considerations

For migration FROM:

  • Assert.AreEqual(expected, actual) → actual.Should().Be(expected)
  • Assert.IsTrue(condition) → condition.Should().BeTrue()
  • Assert.IsInstanceOfType(obj, typeof(MyClass)) → obj.Should().BeOfType<MyClass>()
  • Assert.ThrowsException<E>(action) → action.Should().Throw<E>()

Migration cost: low (mechanical). Migration benefit: richer failure messages + chainable assertions.

Anti-patterns

Anti-patternWhy it failsFix
Mix Assert.X and .Should() styles in same suiteReader confusionPick one + lint enforcement
Long BeEquivalentTo chains without optionsCompares fields you don't care about; brittleUse Excluding to scope (Step 6)
Ship v8+ in a commercial project without a paid licenseLicense violationBuy a v8+ license or pin v7 (Step 1)
value.Should().Be(true) instead of BeTrue()Loses semantic clarityUse BeTrue() (Step 3)
Skip WithMessage on exception assertionsPass for wrong exception typeAlways specify expected message (Step 3)

Limitations

  • Some edge cases in BeEquivalentTo (cyclic refs, polymorphism) need explicit options.
  • .Should() extension can clash with other libraries' extensions (rare).
  • F#-friendly but C#-first; F# usage less ergonomic.

References

  • fa (opens in new window) - FluentAssertions landing
  • fa-intro (opens in new window) - Introduction guide
  • fluentassertions.com/objectgraphs - BeEquivalentTo deep dive
  • github.com/fluentassertions/fluentassertions - repository
  • v7 license note: github.com/fluentassertions/fluentassertions/discussions
  • xunit-tests, nunit-tests, mstest-tests - sister tools (test runners)
  • test-code-conventions

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 equality

Numeric

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.