skills/testing/unit-testing/SKILL.md
Use when writing unit tests with xUnit, NSubstitute/Moq, or FluentAssertions.
npx skillsauth add faysilalshareef/dotnet-ai-kit unit-testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
{MethodName}_When{Condition}_Should{Expected}namespace {Company}.{Domain}.Tests.Unit;
public sealed class OrderServiceTests
{
private readonly IOrderRepository _repository = Substitute.For<IOrderRepository>();
private readonly IUnitOfWork _unitOfWork = Substitute.For<IUnitOfWork>();
private readonly OrderService _sut;
public OrderServiceTests()
{
_sut = new OrderService(_repository, _unitOfWork);
}
[Fact]
public async Task GetOrderAsync_WhenOrderExists_ShouldReturnOrder()
{
// Arrange
var orderId = Guid.NewGuid();
var expected = new Order { Id = orderId, CustomerName = "Test" };
_repository.FindAsync(orderId, Arg.Any<CancellationToken>())
.Returns(expected);
// Act
var result = await _sut.GetOrderAsync(orderId);
// Assert
result.Should().NotBeNull();
result!.CustomerName.Should().Be("Test");
}
[Fact]
public async Task GetOrderAsync_WhenOrderNotFound_ShouldReturnNull()
{
// Arrange
_repository.FindAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
.Returns((Order?)null);
// Act
var result = await _sut.GetOrderAsync(Guid.NewGuid());
// Assert
result.Should().BeNull();
}
}
public sealed class CreateOrderHandlerTests
{
private readonly ICommitEventService<OrderEventData> _commitService =
Substitute.For<ICommitEventService<OrderEventData>>();
private readonly CreateOrderHandler _sut;
public CreateOrderHandlerTests()
{
_sut = new CreateOrderHandler(_commitService);
}
[Fact]
public async Task Handle_ShouldCreateOrderAndCommitEvents()
{
// Arrange
var command = new CreateOrderCommand("Test Customer", 100m, []);
// Act
var result = await _sut.Handle(command, CancellationToken.None);
// Assert
result.Id.Should().NotBeEmpty();
result.Sequence.Should().Be(1);
await _commitService.Received(1).CommitAsync(
Arg.Any<Guid>(),
Arg.Is<IReadOnlyList<Event<OrderEventData>>>(events =>
events.Count == 1),
Arg.Any<CancellationToken>());
}
}
// Collection assertions
orders.Should().HaveCount(3);
orders.Should().Contain(o => o.CustomerName == "Test");
orders.Should().BeInDescendingOrder(o => o.Total);
orders.Should().AllSatisfy(o => o.Status.Should().Be(OrderStatus.Active));
// Object assertions
order.Should().BeEquivalentTo(expected, options =>
options.Excluding(o => o.RowVersion));
// Exception assertions
var act = () => order.Complete();
act.Should().Throw<InvalidOperationException>()
.WithMessage("*already completed*");
// Async exception assertions
var act = async () => await handler.Handle(command, CancellationToken.None);
await act.Should().ThrowAsync<OrderNotFoundException>();
[Theory]
[InlineData("", false)]
[InlineData("Valid Name", true)]
[InlineData(null, false)]
public void Validate_CustomerName_ShouldReturnExpectedResult(
string? name, bool expectedValid)
{
// Arrange
var validator = new CreateOrderValidator();
var command = new CreateOrderCommand(name!, 100m, []);
// Act
var result = validator.Validate(command);
// Assert
result.IsValid.Should().Be(expectedValid);
}
| Anti-Pattern | Correct Approach |
|---|---|
| Testing private methods directly | Test through public API |
| Multiple unrelated assertions | One logical assertion per test |
| Brittle mock verification order | Verify behavior, not call sequence |
| Test names like Test1, TestCreate | Descriptive: MethodName_When_Should |
| Sharing mutable state between tests | Fresh instances per test (constructor) |
# Find test projects
find . -name "*Tests*.csproj" -type f
# Find xUnit usage
grep -r "\[Fact\]\|\[Theory\]" --include="*.cs" tests/
# Find mocking framework
grep -r "Substitute.For\|Mock<\|new Mock" --include="*.cs" tests/
# Find FluentAssertions
grep -r "\.Should()" --include="*.cs" tests/
src/X/ maps to tests/X.Tests/Testsdata-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.