For the complete documentation index, see llms.txt. This page is also available as Markdown.

Migrating from FluentAssertions

This guide is for teams moving from FluentAssertions to Shouldly, and targets Shouldly 5.x.

Most of a migration is mechanical: value.Should().Be(x) becomes value.ShouldBe(x). The value is in the handful of places where the two libraries behave differently. Those are the cases that make a green FA test fail, or silently pass, after a rename. This guide focuses on them.

The mental model

FluentAssertions
Shouldly

Import

using FluentAssertions;

using Shouldly;

Entry point

value.Should().Be(x)

value.ShouldBe(x)

Failure type

the host test framework's assert exception (or AssertionFailedException)

always ShouldAssertException

Chaining

.And / .Which fluent chain

separate statements (some assertions return a value)

There is no .Should() gateway. Every assertion is an extension method directly on the value, named Should…. Most return void. A few return something useful for a follow-up assertion:

InvalidOperationException ex = action.ShouldThrow<InvalidOperationException>();
Cat cat = animal.ShouldBeOfType<Cat>();          // also ShouldBeAssignableTo<T>
string name = maybeNull.ShouldNotBeNull();       // returns the non-null value
Order only = orders.ShouldHaveSingleItem();      // returns the single element

Shouldly puts the code you asserted on into the failure message. In v5 that expression is captured by the compiler (CallerArgumentExpression), so you get the source text in the message without any runtime source lookup:

var result = Add(2, 2);
result.ShouldBe(5);
// -> result should be 5 but was 4

Quick reference

Direct name mappings. Where a mapping is not a plain rename, a note in parentheses says what changed.

Equality and identity

FluentAssertions
Shouldly

x.Should().Be(y)

x.ShouldBe(y)

x.Should().NotBe(y)

x.ShouldNotBe(y)

x.Should().BeNull()

x.ShouldBeNull()

x.Should().NotBeNull()

x.ShouldNotBeNull()

x.Should().BeTrue()

x.ShouldBeTrue()

x.Should().BeFalse()

x.ShouldBeFalse()

x.Should().BeSameAs(y)

x.ShouldBeSameAs(y)

x.Should().NotBeSameAs(y)

x.ShouldNotBeSameAs(y)

x.Should().BeOfType<T>()

x.ShouldBeOfType<T>()

x.Should().NotBeOfType<T>()

x.ShouldNotBeOfType<T>()

x.Should().BeAssignableTo<T>()

x.ShouldBeAssignableTo<T>()

x.Should().BeOneOf(a, b)

x.ShouldBeOneOf([a, b]) (takes an array, not params)

x.Should().BeEquivalentTo(y)

x.ShouldBeEquivalentTo(y) (see caveats)

Comparisons and ranges

FluentAssertions
Shouldly

x.Should().BeGreaterThan(y)

x.ShouldBeGreaterThan(y)

x.Should().BeGreaterThanOrEqualTo(y)

x.ShouldBeGreaterThanOrEqualTo(y)

x.Should().BeLessThan(y)

x.ShouldBeLessThan(y)

x.Should().BeLessThanOrEqualTo(y)

x.ShouldBeLessThanOrEqualTo(y)

x.Should().BePositive()

x.ShouldBePositive()

x.Should().BeNegative()

x.ShouldBeNegative()

x.Should().BeInRange(lo, hi)

x.ShouldBeInRange(lo, hi)

x.Should().NotBeInRange(lo, hi)

x.ShouldNotBeInRange(lo, hi)

date.Should().BeCloseTo(y, precision)

date.ShouldBe(y, tolerance)

date.Should().BeAfter(y)

date.ShouldBeGreaterThan(y)

date.Should().BeBefore(y)

date.ShouldBeLessThan(y)

num.Should().BeApproximately(y, tol)

num.ShouldBe(y, tol)

Collections

FluentAssertions
Shouldly

c.Should().Contain(item)

c.ShouldContain(item)

c.Should().NotContain(item)

c.ShouldNotContain(item)

c.Should().Contain(x => …)

c.ShouldContain(x => …)

c.Should().BeEmpty()

c.ShouldBeEmpty()

c.Should().NotBeEmpty()

c.ShouldNotBeEmpty()

c.Should().HaveCount(n)

c.ShouldHaveCount(n)

c.Should().HaveCountGreaterThan(n)

c.Count.ShouldBeGreaterThan(n) (no ShouldHaveCountGreaterThan)

c.Should().Equal(a, b, c)

c.ShouldBe([a, b, c]) (order-sensitive)

c.Should().BeEquivalentTo(other)

c.ShouldBeEquivalentTo(other, new EquivalencyOptions { IgnoreOrder = true }) (structural + unordered; see below)

c.Should().OnlyHaveUniqueItems()

c.ShouldBeUnique() (not ShouldAllBeUnique)

c.Should().OnlyContain(x => …)

c.ShouldAllBe(x => …)

c.Should().AllBeAssignableTo<T>()

c.ShouldAllBe(x => x is T) (no ShouldAllBeAssignableTo)

c.Should().AllSatisfy(x => x.Should()…)

Should.Satisfy([.. c.Select(x => new Action(() => …))]) (assertion per element, all failures aggregated; see below)

c.Should().ContainSingle()

c.ShouldHaveSingleItem()

c.Should().ContainSingle().Which.Should().Be(v)

c.ShouldHaveSingleItem().ShouldBe(v)

c.Should().BeSubsetOf(other)

c.ShouldBeSubsetOf(other)

c.Should().BeInAscendingOrder()

c.ShouldBeInOrder()

c.Should().BeInDescendingOrder()

c.ShouldBeInOrder(SortDirection.Descending)

c.Should().ContainInOrder(a, b)

no equivalent; see below

c.Should().ContainEquivalentOf(item)

no equivalent for collections; see below

Dictionaries

FluentAssertions
Shouldly

d.Should().ContainKey(k)

d.ShouldContainKey(k)

d.Should().NotContainKey(k)

d.ShouldNotContainKey(k)

d.Should().Contain(k, v)

d.ShouldContainKeyAndValue(k, v)

d.Should().ContainValue(v)

d.Values.ShouldContain(v) (no ShouldContainValue)

d.Should().NotContainValue(v)

d.Values.ShouldNotContain(v) (no ShouldNotContainValue)

Strings

FluentAssertions
Shouldly

s.Should().Be("x")

s.ShouldBe("x") (exact, case-sensitive)

s.Should().Contain("x")

s.ShouldContain("x") (case-sensitive by default, matches FA)

s.Should().ContainEquivalentOf("x")

s.ShouldContain("x", Case.Insensitive) (FA ignores case, so pass Case.Insensitive)

s.Should().StartWith("x")

s.ShouldStartWith("x") (case-sensitive by default, matches FA)

s.Should().EndWith("x")

s.ShouldEndWith("x") (case-sensitive by default, matches FA)

s.Should().Match("re*ex")

s.ShouldMatch(regex) (Shouldly takes a regex, FA Match takes a wildcard)

s.Should().MatchRegex("re.ex")

s.ShouldMatch("re.ex")

s.Should().BeNullOrEmpty()

s.ShouldBeNullOrEmpty()

s.Should().BeNullOrWhiteSpace()

s.ShouldBeNullOrWhiteSpace()

Exceptions

FluentAssertions
Shouldly

act.Should().Throw<T>()

act.ShouldThrow<T>()

act.Should().ThrowExactly<T>()

act.ShouldThrow<T>() then ex.ShouldBeOfType<T>() (no ShouldThrowExactly)

act.Should().NotThrow()

act.ShouldNotThrow()

await act.Should().NotThrowAsync()

await act.ShouldNotThrowAsync()

(await act.Should().ThrowAsync<T>())

await act.ShouldThrowAsync<T>()

act.Should().Throw<T>().WithMessage("x*")

act.ShouldThrow<T>().Message.ShouldContain("x") (no WithMessage)

Behavioral differences to watch for

These are the traps. Read them before you trust a bulk find-and-replace.

Strings are case-sensitive by default

Shouldly's string ShouldContain, ShouldStartWith, ShouldEndWith (and their Not… forms) are case-sensitive by default, matching FluentAssertions' Contain, StartWith, and EndWith, so a straight rename preserves the behavior:

When you do want a case-insensitive comparison — for example FA's ContainEquivalentOf, which ignores case — pass Case.Insensitive:

ShouldBe is strongly typed

FluentAssertions compares through object, so it happily accepts mismatched types and converts them. Shouldly's ShouldBe<T> requires the actual and expected values to be the same type, so mismatches are caught by the compiler, not at runtime:

This is usually a good thing, since it turns sloppy tests into compile errors, but it means some FA assertions that compiled will not. Fix the types (cast, or make both sides T?) rather than fighting it.

Collections: order and element equality

ShouldBe on a collection is order-sensitive and compares elements with their normal equality:

So the FluentAssertions collection methods map like this:

  • Should().Equal(…) (ordered, element equality) becomes ShouldBe(…)

  • Should().BeEquivalentTo(…) (unordered, structural) becomes ShouldBeEquivalentTo(…, new EquivalencyOptions { IgnoreOrder = true })

ShouldBe(…, ignoreOrder: true) also compares order-insensitively, but it matches elements by their Equals, so it only lines up with FA's collection BeEquivalentTo when the elements are values (or override equality). For collections of reference types that should be compared structurally, use ShouldBeEquivalentTo with IgnoreOrder (see the object equivalence section).

Because ShouldBe uses each element's Equals, a collection of reference types that don't override equality is compared by reference:

Chaining: .And and .Which

FluentAssertions chains with .And and drills in with .Which. Shouldly has neither. Split a .And chain into separate statements, and for .Which use the value that some assertions return.

(ShouldContain on a string is case-sensitive, matching FA's .Contain; add Case.Insensitive if you want to ignore case.)

Custom messages (because)

Every FluentAssertions assertion accepts a reason with format arguments, which FA weaves into the failure sentence. The Shouldly counterpart is the customMessage parameter, a plain string with no format arguments, so use interpolation:

The message is appended to the failure output under "Additional Info":

Per-element assertions (AllSatisfy)

FA's AllSatisfy runs an assertion action against every element and reports every failing element at once. It is not the same as ShouldAllBe, which takes a boolean predicate. Map each one to the right tool:

If you do not need the aggregated report, a plain foreach of assertions also works, but it stops at the first failing element rather than listing them all:

No drop-in for ContainInOrder or ContainEquivalentOf

Two collection assertions have no Shouldly counterpart:

  • ContainInOrder(a, b, c) asserts the items appear in that relative order (gaps allowed). There is no built-in; a small local helper covers it:

  • ContainEquivalentOf(item) on a collection (structural match of an element) has no equivalent. Assert with ShouldContain(x => …) on the members you care about, or loop. On a string it just means a case-insensitive substring, which is ShouldContain(x, Case.Insensitive).

Object equivalence: ShouldBeEquivalentTo

ShouldBeEquivalentTo walks the object graph and compares public fields and properties recursively. Like FA's BeEquivalentTo, it is direction-sensitive: comparison is driven by the expected value's members, and any extra members the actual value carries are ignored. It does not require the two sides to be the same type, so two structurally identical objects of different types are equivalent:

Because the expectation drives member selection, you assert a subset by passing an anonymous type (or any type) that carries only the members you care about. This is the direct replacement for FA's "project to a shape and compare" pattern:

A member present on the expected value but missing on the actual value is a failure, and every difference is collected rather than stopping at the first:

Options

A second overload takes an EquivalencyOptions:

  • MembersToIgnore is the counterpart to FA's .Excluding(...), matched by member name anywhere in the graph.

  • IgnoreOrder makes sequences compare order-insensitively; sets and dictionaries are always compared unordered/by key.

What still differs from FA

  • Sequences are ordered by default. Opt into IgnoreOrder for FA's unordered collection behavior.

  • Equals overrides on complex types are ignored — comparison is always member-wise, so a type with a custom Equals is still compared property-by-property. (Well-known value-semantic types such as string, Guid, DateTime, and Uri are treated as leaves and compared with Equals.)

  • Comparers and tolerances — a fluent .WithAutoConversion() switch, .Using<T>(...) custom comparers, or approximate numeric/DateTime matching — are not exposed as options yet. Numeric leaves are auto-converted across kinds, so int 5 is equivalent to long 5 or double 5.0.

For the rare test that needs an FA feature with no counterpart today (custom comparison rules, member selection by predicate), assert the members individually with ShouldSatisfy so you still get every failure at once, or keep a dedicated equivalence library for those cases.

Exceptions and messages

ShouldThrow<T>() returns the caught exception, so assert on its message directly. There is no WithMessage:

Async is the same shape. ShouldThrowAsync<T> returns a Task<T>, so await it:

Like FA's Throw<T>, ShouldThrow<T> matches derived exception types: asserting ShouldThrow<ArgumentException>() is satisfied by an ArgumentNullException. There is no ThrowExactly; if you need an exact type, check it explicitly:

Grouping assertions (FA's AssertionScope)

FluentAssertions uses using (new AssertionScope()) to report several failures together. Shouldly does not have assertion scopes; use ShouldSatisfy (or the static Should.Satisfy for unrelated conditions), which runs every condition and reports all failures at once:

ShouldSatisfyAllConditions still exists but is obsolete in v5: it can't capture the asserted expression and isn't trimming/AOT-safe. Prefer ShouldSatisfy or Should.Satisfy.

Features without a direct equivalent

FluentAssertions
Shouldly today

BeEquivalentTo(…).Excluding(…)

ShouldBeEquivalentTo(…, new EquivalencyOptions { MembersToIgnore = { … } })

BeEquivalentTo(…) with custom comparers / .WithAutoConversion

No fluent equivalent yet; assert members with ShouldSatisfy, or use an anonymous-type subset (see above)

ThrowExactly<T>()

ShouldThrow<T>() + ex.ShouldBeOfType<T>()

.WithMessage("x*")

ex.Message.ShouldContain("x") or ShouldMatch(regex)

AssertionScope

ShouldSatisfy or Should.Satisfy

.And / .Which chaining

Separate statements, or use the value a Should… returns

SatisfyRespectively(…)

ShouldSatisfy with one condition per element (indexed manually)

ContainInOrder(…) / ContainEquivalentOf(…)

Implement<TInterface>()

typeof(IFoo).IsAssignableFrom(typeof(MyType)).ShouldBeTrue()

Last updated