---
name: sdlc-dotnet-testing
description: |
xUnit, Moq/NSubstitute, FluentAssertions, and coverlet patterns for any .NET project. Covers test structure ([Fact]/[Theory]/[InlineData]), mocking discipline, fluent assertions, integration test setup with WebApplicationFactory, and coverage measurement. Stack-agnostic — referenced by every .NET plugin in the marketplace.
Use this skill to:
- Write clear, maintainable unit tests with xUnit [Fact] and [Theory].
- Mock dependencies with Moq or NSubstitute without overusing mocks.
- Write expressive assertions with FluentAssertions.
- Measure coverage with coverlet and enforce a minimum threshold.
Do NOT use this skill for:
- ASP.NET Core-specific integration tests (WebApplicationFactory, HttpClient — those are in sdlc-aspnet-conventions).
- EF Core in-memory or SQL Server LocalDB test patterns (sdlc-efcore-patterns).
- C# language idioms — see sdlc-csharp-conventions.
paths: ["**/*Tests/**", "**/*Test*.cs"]
---
# .NET Testing Patterns (stack-agnostic)
## Test framework: xUnit
xUnit is the primary test framework for .NET. Use NUnit or MSTest only when the project already uses them — do not introduce xUnit into a project that uses another framework.
### NuGet packages for a test project
```xml
net8.0falsetrueallall
```
## xUnit fundamentals
```csharp
using FluentAssertions;
using Moq;
using Xunit;
public class UserServiceTests
{
private readonly Mock _repoMock;
private readonly UserService _sut;
public UserServiceTests()
{
_repoMock = new Mock(MockBehavior.Strict);
_sut = new UserService(_repoMock.Object);
}
[Fact]
public async Task RegisterAsync_WithValidData_ReturnsActiveUser()
{
// Arrange
var command = new RegisterUserCommand("alice@example.com", "Secret1!");
_repoMock.Setup(r => r.ExistsByEmailAsync("alice@example.com", default))
.ReturnsAsync(false);
_repoMock.Setup(r => r.SaveAsync(It.IsAny(), default))
.ReturnsAsync((User u, CancellationToken _) => u);
// Act
var user = await _sut.RegisterAsync(command);
// Assert
user.Email.Should().Be("alice@example.com");
user.IsActive.Should().BeTrue();
_repoMock.VerifyAll();
}
[Fact]
public async Task RegisterAsync_WithDuplicateEmail_ThrowsDomainException()
{
_repoMock.Setup(r => r.ExistsByEmailAsync(It.IsAny(), default))
.ReturnsAsync(true);
await _sut.Invoking(s => s.RegisterAsync(new RegisterUserCommand("dup@example.com", "pass")))
.Should().ThrowAsync()
.WithMessage("*already registered*");
}
}
```
**Test method naming:** `MethodName_Condition_ExpectedOutcome` — readable without additional comments.
**xUnit constructor vs `[Theory]` setup:** Use the constructor for shared setup of the system under test; use `[ClassFixture]` for expensive shared resources (DB connections, servers) that are reused across tests in the class.
## Parameterised tests — [Theory]
```csharp
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void IsValidEmail_BlankInput_ReturnsFalse(string? input)
{
EmailValidator.IsValid(input).Should().BeFalse();
}
[Theory]
[InlineData("alice@example.com", true)]
[InlineData("not-an-email", false)]
[InlineData("@nodomain", false)]
public void IsValidEmail_VariousInputs_MatchesExpected(string email, bool expected)
{
EmailValidator.IsValid(email).Should().Be(expected);
}
// MemberData for complex objects
[Theory]
[MemberData(nameof(InvalidCommands))]
public async Task RegisterAsync_InvalidCommand_ThrowsValidationException(RegisterUserCommand command)
{
await _sut.Invoking(s => s.RegisterAsync(command))
.Should().ThrowAsync();
}
public static IEnumerable