Write, refactor, and review unit tests using xUnit and NSubstitute. Target trustworthy, maintainable, readable tests; AAA structure; isolate dependencies; prefer state-based testing; avoid brittle interaction tests; enforce clear naming and minimal noise.
Consult CHEATSHEET.md at the points marked below.
Test Structure: Arrange-Act-Assert (blank lines separate sections)
Naming: [MethodUnderTest]_[TestScenario]_[ExpectedBehavior]
SUT Variable: Always target
Mocks: ONE per test (verify interactions)
Stubs: Many OK (return data)
Fake Classes: Fake{Interface} - Never shared, always are private into the test class
Fake Variables: {name}Stub (returns data), {name}Mock (verified in assertions)
Assertion syntax: → CHEATSHEET.md ## FluentAssertions
Test Projects (.csproj): When you need to create a test project → CHEATSHEET.md ## Test Project File Template
Fake Test Double: When you need to create a fake test double → CHEATSHEET.md
When reviewing a test project → CHEATSHEET.md
## Template: Fake Test Double (Stub + Mock)## Approved PackagesA unit test should be:
collection[0].Property is wrong; use Contains, BeEquivalentTo, or ContainSingle(predicate)Assert.Contains(list, x => x.Name == "John" && x.Age == 30) not separate Assert.Equal calls on each property[Fact]
public void Withdraw_ValidAmount_DecreasesBalance()
{
var account = new BankAccount(100);
var withdrawAmount = 50;
account.Withdraw(withdrawAmount);
Assert.Equal(50, account.Balance);
}
Rule: Never access a collection by index in an assertion.
// ✅
Assert.Contains(repo.GetAddedEntities<Customer>(),
c => c.FirstName == "John" && c.LastName == "Doe");
// ❌ — brittle, order-dependent
Assert.Equal("John", repo.GetAddedEntities<Customer>()[0].FirstName);
Assert.Equal("Doe", repo.GetAddedEntities<Customer>()[0].LastName);
// ✅ — order-insensitive, property-scoped
var expectedCustomers = new[]
{
new Customer { FirstName = "John", LastName = "Doe" },
new Customer { FirstName = "Jane", LastName = "Smith" }
};
repo.GetAddedEntities<Customer>().Should().BeEquivalentTo(expectedCustomers,
options => options
.Including(c => c.FirstName)
.Including(c => c.LastName)
);
// ❌ — order-dependent, over-specifies all properties, breaks on any schema change
result.Should().BeEquivalentTo(new Customer { Id = 1, FirstName = "John", ... });
// ✅
repo.GetAddedEntities<Customer>()
.Should().ContainSingle(c => c.FirstName == "John");
// ✅ — one call, one failure message
Assert.True(result.TotalProcessed == 3 && result.Added == 2,
"Expected 3 processed and 2 added.");
// Or as a private helper
private static void AssertImportResult(CustomerImportResult result, int processed, int added)
=> Assert.True(result.TotalProcessed == processed && result.Added == added,
$"Expected {processed} processed, {added} added.");
Stub — provides controlled input to the SUT. Never fails a test. Mock — verifies the SUT interacted correctly with a dependency. Can fail a test.
// Stub — returns data, never verified
var calculatorStub = Substitute.For<ICalculator>();
calculatorStub.Calculate(Arg.Any<int>()).Returns(150m);
var result = target.ProcessOrder(order);
Assert.Equal(150m, result.Total); // asserting result, not the stub
// Mock — interaction is the assertion
var emailMock = Substitute.For<IEmailService>();
target.ProcessOrder(order);
emailMock.Received(1).SendEmail(Arg.Any<string>()); // asserting the mock
Golden Rule: ONE mock per test — if you're verifying multiple mocks, you're testing multiple things.
[MethodUnderTest]_[TestScenario]_[ExpectedBehavior]
Examples:
IsValidFileName_BadExtension_ReturnsFalse()Add_NegativeNumber_ThrowsException()ProcessOrder_InvalidUser_SendsNotificationEmail()Guidelines:
target as the variable name for the SUT instance in testsThere can be only one target per test.
Class Names:
Fake{InterfaceName} prefix for handwritten test doubles that can act as both stub and mockFakeRepository, FakeUnitOfWork, FakePersonServiceVariable Names in Tests:
{name}Stub suffix when the fake provides indirect input (returns data){name}Mock suffix when the fake is verified in assertions (checks interactions)// ✅ CORRECT: Variable named as stub (only returns data)
var calculatorStub = GetCalculatorStub({InputData});
var target = GetTarget(calculatorStub);
// assert is not verifying calculatorStub, so "stub" is correct
// ✅ CORRECT: Variable named as mock (verified in assertion)
var emailMock = Substitute.For<IEmailService>();
// ... act on target...
emailMock.Received(1).SendEmail(Arg.Any<string>()); // Verifying emailMock
// ❌ WRONG: Generic naming doesn't show intent
var repository = Substitute.For<IRepository>(); // Is this a stub or mock?
// ❌ WRONG: Calling it "mock" but using as stub
var repositoryMock = Substitute.For<IRepository>();
repositoryMock.GetAll().Returns(data);
var result = service.Process(); // Not verifying repositoryMock - should be named repositoryStub
Key Rule: The suffix tells the reader HOW the fake is used in THIS test, not what it's capable of.
Default: Solitary (full isolation) — fake ALL dependencies. Use this codebase default because services have many dependencies, the repository pattern abstracts EF Core, and layers should stay cleanly separated.
Exception: Collaborative (partial isolation) — use real collaborators only when they have no dependencies (no I/O, or external services) or are simple value objects.
| Solitary | Collaborative | |
|---|---|---|
IRepository | Always fake | Never real |
IUnitOfWork | Always fake | Never real |
| Pure math / validators | Fake OK | Real OK |
| Value objects | Fake OK | Real OK |
// Solitary — everything faked
var calculatorStub = Substitute.For<ICalculator>();
var repositoryStub = Substitute.For<IRepository>();
OrderService target = GetTarget(calculatorStub, repositoryStub);
// Collaborative — real only when no dependencies/stateless
var calculator = new PriceCalculator(); // Real - it's just math
var validator = new OrderValidator(); // Real - no dependencies
var repositoryStub = new FakeRepository(); // Fake - has I/O
OrderService target = GetTarget(calculator, validator, repositoryStub);
| Use | When |
|---|---|
| Handwritten Fake | Needs state tracking, query methods for assertions, or both stub + mock behavior |
| NSubstitute | Simple return value, one-off interaction check, no state needed |
// Handwritten Fake — tracks state, inspectable in assertions
var repoMock = new FakeUnitOfWork(existingCustomers);
target.ImportPersons();
Assert.Contains(repoMock.GetAddedEntities(), c => c.FirstName == "John");
// NSubstitute — simple stub or one-off verify
var emailMock = Substitute.For<IEmailService>();
target.ProcessOrder(order);
emailMock.Received(1).SendEmail(Arg.Any<string>());
src/
└── Modules/
└── {Module}/
└── {Module}.{Assembly}/
└── {Class}.cs
└── {Module}.{Assembly}.UnitTests/
└── {Class}Tests.cs
{ClassName}Tests// filepath: Modules/{Module}/{Module}.{AssemblyName}.UnitTests/{Class}Tests.cs
using DataAccess;
using FluentAssertions;
using {Module}.DataModel;
using Contracts.{Module};
namespace {Module}.{AssemblyName}.UnitTests;
public class {Class}Tests
{
// Assert on result
[Fact]
public void {Method}_{Scenario}_{ExpectedBehavior}()
{
var {inputItem} = Create{Entity}({testData});
var target = GetTarget(new[] { {inputItem} }, new {EntityType}[0]);
var result = target.{Method}();
Assert.Equal({expectedValue}, result.{Property});
}
// Assert on mock (interaction/state)
[Fact]
public void {Method}_{Scenario}_{ExpectedBehavior}()
{
var repoMock = new Fake{Dependency}(new {EntityType}[0]);
var {inputItem} = Create{Entity}({testData});
var target = GetTarget(new[] { {inputItem} }, repoMock);
target.{Method}();
Assert.Contains(repoMock.Get{TrackedEntities}<{EntityType}>(),
e => e.{Property1} == {expectedValue} && e.{Property2} == {expectedValue});
}
// Assert on full collection match
[Fact]
public void {Method}_{Scenario}_{ExpectedBehavior}()
{
var repoMock = new Fake{Dependency}(new {EntityType}[0]);
var target = GetTarget(new[] { Create{Entity}({data1}), Create{Entity}({data2}) }, repoMock);
target.{Method}();
var expected = new[]
{
Create{Entity}({expectedData1}),
Create{Entity}({expectedData2})
};
repoMock.Get{TrackedEntities}<{EntityType}>().Should().BeEquivalentTo(expected,
options => options.Including(e => e.{Property1}).Including(e => e.{Property2})
);
}
// ── Helpers ────────────────────────────────────────────────
private static {Class} GetTarget({InputType}[] inputData, {DependencyDataType}[] dependencyData)
=> GetTarget(new Fake{Dependency1}(inputData), new Fake{Dependency2}(dependencyData));
private static {Class} GetTarget({InputType}[] inputData, Fake{Dependency2} repoMock)
=> GetTarget(new Fake{Dependency1}(inputData), repoMock);
private static {Class} GetTarget(Fake{Dependency1} dep1, Fake{Dependency2} dep2)
=> new {Class}(dep1, dep2);
private static {EntityType} Create{Entity}({ParamType} param1, {ParamType} param2, {ParamType} param3)
=> new {EntityType} { {Property1} = param1, {Property2} = param2, {Property3} = param3 };
private static void Assert{ConditionName}({ResultType} result)
=> Assert.True(result.{Property1} == {expectedValue} && result.{Property2} == {expectedValue},
"Expected {description}.");
}
✅ CORRECT: Separate tests for separate expectations
[Fact]
public void ImportPersonsAsCustomers_NewPerson_AddsCustomer()
{
var repoMock = new FakeUnitOfWork(new Customer[0]);
var person = CreatePersonData(1, "John", "Doe", DateTime.UtcNow);
var target = GetTarget(new[] { person }, repoMock);
target.ImportPersonsAsCustomers();
Assert.Contains(repoMock.GetAddedEntities<Customer>(),
c => c.FirstName == "John" && c.LastName == "Doe");
}
[Fact]
public void ImportPersonsAsCustomers_NewPerson_ReturnsResultWithAddedCustomer()
{
var person = CreatePersonData(1, "John", "Doe", DateTime.UtcNow);
var target = GetTarget(new[] { person }, new Customer[0]);
CustomerImportResult result = target.ImportPersonsAsCustomers();
Assert.Equal(1, result.CustomersAdded);
}
// ❌ WRONG: Testing implementation details
repository.Received(1).GetByIdAsync(entityId, CancellationToken);
repository.Received(1).Add(Arg.Any<Entity>());
// ... testing every single call
// ✅ CORRECT: Testing outcomes
result.IsSuccess.Should().BeTrue();
result.Value.Id.Should().NotBeEmpty();
// ❌ WRONG: Shared mutable state
private Entity _sharedEntity; // Modified by tests, causes flaky tests
// ✅ CORRECT: Fresh setup per test
[Fact]
public void Test()
{
var entity = CreateEntity(); // Fresh instance
}
// ❌ WRONG: Logic in tests
[Fact]
public void BadTest()
{
for (int i = 0; i < 10; i++)
{
if (i % 2 == 0)
{
// test logic
}
}
}
[Theory] vs [Fact]Use [Theory] only when testing the same logical path with different data values (e.g. boundary checks, math).
Use separate [Fact] tests when each case represents a different business scenario with its own name and meaning.
// ✅ [Theory] — same logic, different inputs
[Theory]
[InlineData(0, true)]
[InlineData(2, true)]
[InlineData(1, false)]
public void IsEven_Number_ReturnsCorrectResult(int number, bool expected)
{
// ... arrange target ...
var actual = target.IsEven(number);
Assert.Equal(expected, actual);
}
// ❌ [Theory] misuse — different scenarios collapsed into one poor name
[Theory]
[InlineData("[email protected]", true)]
[InlineData("", false)]
[InlineData(null, false)]
public void Validate_Input_ReturnsResult(string input, bool expected) { ... }
// ✅ Correct — separate [Fact] per scenario with meaningful names
[Fact]
public void ValidateEmail_ValidFormat_ReturnsTrue() { ... }
[Fact]
public void ValidateEmail_EmptyString_ReturnsFalse() { ... }
[Fact]
public void ValidateEmail_Null_ReturnsFalse() { ... }
```37:["$","$L3f",null,{"content":"$40","frontMatter":{"name":"unit-testing","description":"Write, refactor, and review unit tests using xUnit and NSubstitute. Target trustworthy, maintainable, readable tests; AAA structure; isolate dependencies; prefer state-based testing; avoid brittle interaction tests; enforce clear naming and minimal noise.","version":"1.0.0","language":"C#","framework":".NET 10.0","dependencies":"xUnit, NSubstitute, FluentAssertions","pattern":"Arrange-Act-Assert, Test Doubles, Mocks and Stubs"}}]