testing-patterns
Unit testing patterns with xUnit, Moq, and FluentAssertions including AAA pattern, naming conventions, mocking, and edge case coverage. Use when writing or reviewing unit tests.
When & Why to Use This Skill
This Claude skill provides a comprehensive framework for implementing professional unit testing patterns using xUnit, Moq, and FluentAssertions. It streamlines the creation of maintainable and readable test suites by enforcing the Arrange-Act-Assert (AAA) pattern, standardized naming conventions, and robust mocking strategies. Designed to enhance software reliability, it ensures high-quality code through systematic edge-case coverage and clear assertion logic.
Use Cases
- Standardizing unit test structures across a development team using the Arrange-Act-Assert (AAA) pattern for better maintainability.
- Implementing descriptive naming conventions (Method_Scenario_Expected) to make test suites self-documenting and easier to debug.
- Configuring complex mock behaviors and verifying dependency interactions using Moq to isolate the System Under Test (SUT).
- Writing expressive and human-readable assertions with FluentAssertions to improve the clarity of test failure messages.
- Ensuring comprehensive test coverage for critical edge cases such as null returns, empty collections, and input validation failures.
| name | testing-patterns |
|---|---|
| description | Unit testing patterns with xUnit, Moq, and FluentAssertions including AAA pattern, naming conventions, mocking, and edge case coverage. Use when writing or reviewing unit tests. |
Testing Patterns
Overview
Tests ensure code correctness and enable confident refactoring. Follow these patterns for consistent, maintainable tests.
Test Framework Stack
- xUnit - Test framework
- Moq - Mocking library
- FluentAssertions - Readable assertions
Test Structure
Arrange-Act-Assert (AAA)
Every test follows this pattern:
[Fact]
public async Task GetByIdAsync_WithValidId_ReturnsTask()
{
// Arrange
var taskId = Guid.NewGuid();
var expectedTask = new TaskItem { Id = taskId, Title = "Test Task" };
_repositoryMock.Setup(r => r.GetByIdAsync(taskId, It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedTask);
// Act
var result = await _sut.GetByIdAsync(taskId);
// Assert
result.Should().NotBeNull();
result!.Id.Should().Be(taskId);
result.Title.Should().Be("Test Task");
}
Test Class Structure
public class TaskServiceTests
{
private readonly Mock<ITaskRepository> _repositoryMock;
private readonly Mock<ILogger<TaskService>> _loggerMock;
private readonly TaskService _sut; // System Under Test
public TaskServiceTests()
{
_repositoryMock = new Mock<ITaskRepository>();
_loggerMock = new Mock<ILogger<TaskService>>();
_sut = new TaskService(_repositoryMock.Object, _loggerMock.Object);
}
// Tests...
}
Naming Conventions
Test Method Names
Format: MethodName_Scenario_ExpectedResult
// Good - descriptive names
public async Task GetByIdAsync_WithValidId_ReturnsTask()
public async Task GetByIdAsync_WithNonExistentId_ReturnsNull()
public async Task CreateAsync_WithValidRequest_CreatesAndReturnsTask()
public async Task DeleteAsync_WithExistingId_ReturnsTrue()
// Bad - unclear names
public async Task TestGet()
public async Task Test1()
Mocking with Moq
Setup Mock Returns
// Return a value
_repositoryMock.Setup(r => r.GetByIdAsync(taskId, It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedTask);
// Return null
_repositoryMock.Setup(r => r.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((TaskItem?)null);
Verify Mock Calls
_repositoryMock.Verify(r => r.CreateAsync(It.IsAny<TaskItem>(), It.IsAny<CancellationToken>()), Times.Once);
_repositoryMock.Verify(r => r.DeleteAsync(taskId, It.IsAny<CancellationToken>()), Times.Once);
FluentAssertions
Basic Assertions
result.Should().Be(expected);
result.Should().BeNull();
result.Should().NotBeNull();
items.Should().HaveCount(3);
items.Should().BeEmpty();
result.IsCompleted.Should().BeTrue();
Edge Case Coverage
Always Test These Scenarios
- GET: found, not found, empty list
- CREATE: success, validates input, sets defaults
- UPDATE: success, not found, partial update
- DELETE: success, not found