From dotnet
Write, review, or refactor C# unit tests using XUnit v3, NSubstitute, FluentAssertions, and Atc.Test. Use this skill whenever the user asks to create tests, add test coverage, fix failing tests, mock dependencies with NSubstitute, or follow the ClassName_MethodUnderTest_ExpectedBehavior naming convention in a .NET/C# project.
npx claudepluginhub atc-net/atc-agentic-toolkit --plugin dotnetThis skill uses the workspace's default tool permissions.
Follow these practices when writing, reviewing, or refactoring C# unit tests. The stack is XUnit v3 + NSubstitute + FluentAssertions + Atc.Test. Do not use Moq.
Provides C# and .NET testing patterns with xUnit, FluentAssertions, Moq/NSubstitute mocking, Testcontainers integration tests, and AAA structure. For writing, reviewing, or debugging tests.
Writing xUnit tests. v3 Fact/Theory, fixtures, parallelism, IAsyncLifetime, v2 compatibility.
Provides NUnit best practices for C# unit tests, including setup, AAA pattern, data-driven tests with [TestCase]/[TestCaseSource], assertions, and mocking.
Share bugs, ideas, or general feedback.
Follow these practices when writing, reviewing, or refactoring C# unit tests. The stack is XUnit v3 + NSubstitute + FluentAssertions + Atc.Test. Do not use Moq.
When adding tests to an existing project, read nearby test files first and match the established style before applying these defaults.
Name tests ClassName_MethodUnderTest_ExpectedBehavior so the test runner output reads like a specification.
Every test follows the Arrange-Act-Assert pattern with explicit comments:
[Fact]
public void OrderService_CalculateTotal_ReturnsZero_WhenCartIsEmpty()
{
// Arrange
var sut = new OrderService();
var emptyCart = new Cart();
// Act
var result = sut.CalculateTotal(emptyCart);
// Assert
result.Should().Be(0m);
}
IDisposable/IAsyncDisposable for teardown.[ProjectName].TestsMicrosoft.NET.Test.Sdk, xunit (v3), xunit.runner.visualstudioFluentAssertions, NSubstitute, Atc.Test[ClassUnderTest]Tests (e.g., OrderServiceTests for OrderService)dotnet testPrefer FluentAssertions over built-in Assert.* because they produce clearer failure messages and read more naturally.
// Value equality
result.Should().Be(expected);
result.Should().NotBe(unexpected);
// String assertions
name.Should().StartWith("Order");
name.Should().Contain("active");
name.Should().BeNullOrEmpty();
// Collection assertions
items.Should().HaveCount(3);
items.Should().Contain(x => x.IsActive);
items.Should().BeInAscendingOrder(x => x.Name);
items.Should().BeEmpty();
// Null checks
result.Should().NotBeNull();
result.Should().BeNull();
// Type checks
result.Should().BeOfType<OrderConfirmation>();
result.Should().BeAssignableTo<IConfirmation>();
// Exception assertions
var act = () => sut.Process(null!);
act.Should().Throw<ArgumentNullException>()
.WithParameterName("order");
// Async exception assertions
var act = async () => await sut.ProcessAsync(null!);
await act.Should().ThrowAsync<ArgumentNullException>();
Fall back to built-in Assert.* only when FluentAssertions does not cover the scenario.
Use NSubstitute to create test doubles. The goal is to isolate the system under test from its dependencies so each test verifies exactly one unit of behavior.
[Fact]
public async Task OrderService_PlaceOrderAsync_SavesOrder_WhenValid()
{
// Arrange
var repository = Substitute.For<IOrderRepository>();
repository.SaveAsync(Arg.Any<Order>(), Arg.Any<CancellationToken>())
.Returns(Task.CompletedTask);
var sut = new OrderService(repository);
var order = new Order { Id = 1, Total = 99.95m };
// Act
await sut.PlaceOrderAsync(order, CancellationToken.None);
// Assert
await repository.Received(1)
.SaveAsync(Arg.Is<Order>(o => o.Id == 1), Arg.Any<CancellationToken>());
}
Key patterns:
Substitute.For<T>() to create mocks from interfaces.Returns(value) and .ReturnsForAnyArgs(value) to configure return values.Received(n) / .DidNotReceive() to verify interactionsArg.Any<T>(), Arg.Is<T>(predicate) for argument matchingCancellationToken through -- never drop it silentlyUse [Theory] when the same logic needs to be verified against multiple inputs.
[Theory]
[InlineData(0, 0, 0)]
[InlineData(1, 2, 3)]
[InlineData(-1, 1, 0)]
public void Calculator_Add_ReturnsExpectedSum(int a, int b, int expected)
{
// Arrange
var sut = new Calculator();
// Act
var result = sut.Add(a, b);
// Assert
result.Should().Be(expected);
}
[InlineData] for simple inline values[MemberData(nameof(TestData))] for method/property-based data (useful when data is complex or reused)[ClassData(typeof(MyTestData))] for class-based data sourcesXUnit v3 supports async tests natively -- return Task and use async/await:
[Fact]
public async Task UserService_GetByIdAsync_ReturnsNull_WhenUserNotFound()
{
// Arrange
var repo = Substitute.For<IUserRepository>();
repo.GetByIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns((User?)null);
var sut = new UserService(repo);
// Act
var result = await sut.GetByIdAsync(999, CancellationToken.None);
// Assert
result.Should().BeNull();
}
CancellationToken in async method signaturesawait act.Should().ThrowAsync<T>() for async exception testingThe Atc.Test package provides helpers that reduce test boilerplate. Leverage it for common testing utilities and assertions when available in the project.
IClassFixture<T> for shared context within a test class (e.g., database setup)ICollectionFixture<T> for shared context across multiple test classes[Trait("Category", "CategoryName")] for categorization and selective test runsITestOutputHelper for diagnostic output during test execution[Fact(Skip = "reason")] or [Theory(Skip = "reason")]Good test suites include tests for boundary conditions: