Skip to content

/csharp-testing-standards

Defines the testing standards, patterns, and conventions for all C# unit and integration test projects. Rules cover test framework usage, naming, structure, mocking, assertions, and parameterization. Apply these rules uniformly across all test projects.

shell
$ npx -y skills add linuxchata/ai-playbook --skill csharp-testing-standards --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.
  • You can call itInvoke it directly when you want it.
  • Slash command/csharp-testing-standards
How auto-invocation works

Context preview

The summary Claude sees to decide when to auto-load this skill.

Defines the testing standards, patterns, and conventions for all C# unit and integration test projects. Rules cover test framework usage, naming, structure, mocking, assertions, and parameterization. Apply these rules uniformly across all test projects.

SKILL.md

csharp-testing-standards.SKILL.md
name: csharp-testing-standards
description: Defines the testing standards, patterns, and conventions for all C# unit and integration test projects. Rules cover test framework usage, naming, structure, mocking, assertions, and parameterization. Apply these rules uniformly across all test projects.
metadata:
  version: 1.1.0

C# Testing Standards

Description

Defines the testing standards, patterns, and conventions for all C# unit and integration test projects. Rules cover test framework usage, naming, structure, mocking, assertions, and parameterization. Apply these rules uniformly across all test projects.

---

1. Framework & Tooling

  • **Test framework**: NUnit 3
  • **Mocking**: Moq
  • **Coverage**: coverlet.collector
  • **Test runner**: `Microsoft.NET.Test.Sdk` + `NUnit3TestAdapter`

Core NUnit attributes in use:

| Attribute | Purpose | |---|---| | `[TestFixture]` | Marks the test class | | `[Test]` | Marks a single test method | | `[SetUp]` | Runs before each test | | `[TearDown]` | Runs after each test | | `[OneTimeSetUp]` | Runs once before all tests in the fixture | | `[TestCase]` | Parameterized test inline values |

---

2. Test Class Structure

2.1 Visibility

Test classes are `internal`. They do not need to be `public` – NUnit discovers them via the test runner regardless.

[TestFixture]
internal class OrderServiceTests { }

2.2 System Under Test Field

Name the field under test `_sut` (system under test) and initialize it in `[SetUp]`:

private OrderService _sut = null!;

2.3 Mock Fields

Declare all mocks as class-level fields initialized in `[SetUp]`:

private Mock<IOrderRepository> _orderRepositoryMock = null!;
private Mock<ILogger<OrderService>> _loggerMock = null!;

2.4 SetUp Method

  • Initialize all mocks and the SUT in the `[SetUp]` method.
  • Configure **happy-path** default behaviors here so individual tests only override what they specifically need.
  • Keep `[SetUp]` focused – it should not contain assertions or complex logic.
[SetUp]
public void Setup()
{
    _orderRepositoryMock = new Mock<IOrderRepository>();
    _orderRepositoryMock
        .Setup(r => r.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
        .ReturnsAsync((Order?)null);

    _sut = new OrderService(
        _orderRepositoryMock.Object,
        NullLogger<OrderService>.Instance);
}

---

3. Test Method Naming

3.1 Pattern

MethodName_WhenCondition_ThenExpectedBehavior
  • **MethodName** – the method being tested (must match the implementation exactly).
  • **WhenCondition** – the specific input state or pre-condition being exercised.
  • **ThenExpectedBehavior** – the exact outcome that is asserted.
// ✅ Correct
public async Task GetByIdAsync_WhenOrderDoesNotExist_ThenReturnsNull()
public async Task CreateAsync_WhenRequestIsValid_ThenPersistsAndReturnsSuccess()
public void Validate_WhenAmountIsNegative_ThenReturnsInvalidResult()

// ❌ Wrong – vague and non-descriptive
public async Task TestGetOrder()
public async Task CreateOrder_Success()

3.2 Async Test Methods

All async test methods that are `async Task` must also follow the `Async` suffix rule:

// ✅ Correct
public async Task CreateAsync_WhenRequestIsValid_ThenReturnsSuccess()

// ❌ Wrong
public async Task Create_WhenRequestIsValid_ThenReturnsSuccess()

---

4. Arrange / Act / Assert

Every test body must follow the three-section AAA structure, with explicit comments:

[Test]
public async Task GetByIdAsync_WhenOrderExists_ThenReturnsOrder()
{
    // Arrange
    var orderId = Guid.NewGuid();
    var order = new Order { Id = orderId, CustomerName = "Alice" };
    _orderRepositoryMock
        .Setup(r => r.GetByIdAsync(orderId, It.IsAny<CancellationToken>()))
        .ReturnsAsync(order);

    // Act
    var result = await _sut.GetByIdAsync(orderId, It.IsAny<CancellationToken>());

    // Assert
    Assert.That(result, Is.Not.Null);
    Assert.That(result!.CustomerName, Is.EqualTo("Alice"));
}

---

5. Assertions

5.1 Constraint Model

Always use `Assert.That(actual, constraint)` – the NUnit constraint model. Avoid the classic assertion API:

// ✅ Correct – constraint model
Assert.That(result, Is.Not.Null);
Assert.That(result.IsValid, Is.True);
Assert.That(result.Message, Is.Null);
Assert.That(items, Has.Length.EqualTo(2));
Assert.That(items, Is.EquivalentTo(expected));

// ❌ Avoid – classic API
Assert.IsNotNull(result);
Assert.IsTrue(result.IsValid);
Assert.AreEqual(2, items.Length);

5.2 Exception Assertions

Use `Assert.ThrowsAsync<T>` for async methods that are expected to throw:

Assert.ThrowsAsync<ArgumentNullException>(
    () => _sut.CreateAsync(null!, It.IsAny<CancellationToken>()));

5.3 Mock Verification

Use `.Verify()` only when asserting that a side-effecting call was (or was not) made. Do not use `.Verify()` as a substitute for return value assertions:

// ✅ Correct – asserting a save was triggered exactly once
_orderRepositoryMock.Verify(
    r => r.SaveAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()),
    Times.Once);

// ✅ Correct – asserting a call was never made
_orderRepositoryMock.Verify(
    r => r.DeleteAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()),
    Times.Never);

---

6. Parameterized Tests

Use `[TestCase]` for boundary values (null, empty, whitespace, zero, negative) rather than duplicating test logic:

[TestCase(null!)]
[TestCase("")]
[TestCase("   ")]
public async Task CreateAsync_WhenCustomerNameIsEmpty_ThenReturnsFailure(string customerName)
{
    // Arrange
    var request = new CreateOrderRequest { CustomerName = customerName };

    // Act
    var result = await _sut.CreateAsync(request, It.IsAny<CancellationToken>());

    // Assert
    Assert.That(result.IsValid, Is.False);
}

For multiple varying inputs, prefer `[TestCaseSource]` over stacking many `[Tes

Read more
Read it on GitHub ↗

Showing the first part of this file.

Ships withai-playbook

Rules, skills, and guidelines for AI coding assistants – Claude, Cursor, and beyond.

Get the whole plugin, auto-invoked
Stats
6
Stars
0
Views
0
Forks
Maintained
Maintenance
PowerShell
Language
MIT
License
2mo ago
Last commit
3mo ago
Created

Repo: linuxchata/ai-playbook