.NET unit testing (C# / F# / VB.NET) with xUnit.net as the primary framework - `[Fact]` single tests, `[Theory]` + `[InlineData]`/`[ClassData]`/`[MemberData]` parametrization, class and collection fixtures (`IClassFixture` / `ICollectionFixture`), parallel-execution config, `ITestOutputHelper` output, skip/traits filtering, and `dotnet test` CI with trx + coverage. Includes framework choice (xUnit for new projects; match an existing NUnit/MSTest convention detected from csproj PackageReferences; legacy .NET Framework 4.x → NUnit or MSTest) and test-authoring conventions (AAA mapping, argument-order traps, no fabricated methods, no smoke asserts). References cover NUnit (`[TestCase]`, constraint-model `Assert.That`), MSTest (`[TestClass]` / `[DataRow]` / TestContext), and the FluentAssertions `.Should()` catalog including the v8 commercial-license change. Use for any .NET unit-test task: choosing or configuring a framework, writing or parameterizing tests, fixtures, or wiring CI.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Companion reference for dotnet-unit-tests. Per fluentassertions.com,
FluentAssertions is the de facto fluent-assertion library for .NET - it
pairs with xUnit, NUnit, or MSTest (auto-detects the framework and throws
framework-specific exceptions), so assertion code survives a move between
frameworks.
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). Commercial projects either buy a v8+ license or pin to v7; open-source and non-commercial projects can use v8+ free.
dotnet add package FluentAssertions # current v8+ (see license note above)
dotnet add package FluentAssertions --version 7.0.0 # pin v7 for fully-OSS commercial usePer fluentassertions.com/introduction - .Should() is the
fluent entry point:
using FluentAssertions;
result.Should().Be(42);
list.Should().HaveCount(3);// Equality
value.Should().Be(expected);
value.Should().NotBe(expected);
value.Should().BeNull();
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().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().AllSatisfy(x => x.Should().BePositive());
// Type checks
result.Should().BeOfType<Success>();
result.Should().BeAssignableTo<IResult>();
// Boolean
flag.Should().BeTrue();
// Custom predicates
user.Should().Satisfy(u => u.Email.Contains("@") && u.Age >= 18);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();Always specify WithMessage - a type-only assertion passes for the wrong
failure of the right type.
.And chains assertions; .Which accesses the result for further
assertion:
list.Should().HaveCount(3).And.Contain("alice").And.NotContain("eve");
result.Should().BeOfType<Success>()
.Which.Value.Should().Be(42);BeEquivalentTo deep equalityStructural comparison - the most powerful matcher (fluentassertions.com/objectgraphs):
actual.Should().BeEquivalentTo(expected); // deep equal, order-independent
// Across different types (record vs class), excluding fields:
user.Should().BeEquivalentTo(dto, opts => opts
.Excluding(u => u.PasswordHash));
// With options
actual.Should().BeEquivalentTo(expected, opts => opts
.Excluding(x => x.Timestamp)
.ComparingByMembers<MyType>()
.WithStrictOrdering()
);Options: Excluding, Including, ComparingByMembers,
WithStrictOrdering, WithoutStrictOrdering, IgnoringCyclicReferences.
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>()Mechanical, low-cost; the benefit is richer failure messages (object
structure shown, e.g. Expected list to have 4 items, but found 3: ["alice", "bob", "charlie"]) plus chainability.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Mix Assert.X and .Should() in one suite | Reader confusion | Pick one + lint enforcement |
BeEquivalentTo without options on volatile fields | Compares fields you don't care about; brittle | Excluding(...) |
| Ship v8+ commercially without a paid license | License violation | Buy a v8+ license or pin v7 |
value.Should().Be(true) | Loses semantic clarity | BeTrue() / BeFalse() |
BeEquivalentTo edge cases (cyclic refs, polymorphism) need explicit
options..Should() can clash with other libraries' extension methods (rare).