Skip to content
cccskills
FreePublished on GitHub

dotnet-testing

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 aspnet-core-plugin:aspnet-conventions). - EF Core in-memory or SQL Server LocalDB test patterns (aspnet-core-plugin:efcore-patterns). - C# language idioms — see csharp-foundation:csharp-conventions.

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md10.5 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

.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

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="xunit" Version="2.9.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
    <PackageReference Include="Moq" Version="4.20.72" />
    <PackageReference Include="FluentAssertions" Version="6.12.1" />
    <PackageReference Include="coverlet.collector" Version="6.0.2">
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>
</Project>

xUnit fundamentals

using FluentAssertions;
using Moq;
using Xunit;

public class UserServiceTests
{
    private readonly Mock<IUserRepository> _repoMock;
    private readonly UserService _sut;

    public UserServiceTests()
    {
        _repoMock = new Mock<IUserRepository>(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<User>(), 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<string>(), default))
                 .ReturnsAsync(true);

        await _sut.Invoking(s => s.RegisterAsync(new RegisterUserCommand("dup@example.com", "pass")))
                  .Should().ThrowAsync<DomainException>()
                  .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<T>] for expensive shared resources (DB connections, servers) that are reused across tests in the class.

Parameterised tests — [Theory]

[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<ValidationException>();
}

public static IEnumerable<object[]> InvalidCommands =>
[
    [new RegisterUserCommand(null!, "pass")],
    [new RegisterUserCommand("", "pass")],
    [new RegisterUserCommand("user@example.com", "")],
];

FluentAssertions — expressive assertions

Prefer FluentAssertions over xUnit's Assert.* — it gives richer failure messages and fluent chaining.

// Collections
result.Items.Should().HaveCount(3)
    .And.ContainSingle(i => i.Name == "Alice");

// Strings
response.ContentType.Should().StartWith("application/json");

// Exceptions (sync and async)
Action act = () => service.Compute(-1);
act.Should().Throw<ArgumentOutOfRangeException>()
   .WithParameterName("value");

await service.Awaiting(s => s.GetAsync(-1))
    .Should().ThrowAsync<KeyNotFoundException>();

// Numeric / date
price.Should().BeGreaterThan(0).And.BeLessThan(1000);
createdAt.Should().BeCloseTo(DateTimeOffset.UtcNow, precision: TimeSpan.FromSeconds(5));

// Equivalence (structural equality, ignores property order)
result.Should().BeEquivalentTo(expected, opt => opt.Excluding(x => x.Id));

Never use Assert.True(a.Equals(b)) — the failure message shows only "expected true". Use a.Should().Be(b).

Moq — mocking discipline

// Constructor injection — preferred (no reflection, no [Inject])
var repo = new Mock<IUserRepository>(MockBehavior.Strict);
var sut = new UserService(repo.Object);

// Stubbing — be specific with arguments
repo.Setup(r => r.FindAsync(42, default)).ReturnsAsync(testUser);

// Argument matchers when exact value doesn't matter
repo.Setup(r => r.ExistsByEmailAsync(It.IsAny<string>(), default))
    .ReturnsAsync(false);

// Capture arguments for detailed assertions
repo.Setup(r => r.SaveAsync(It.IsAny<User>(), default))
    .Callback<User, CancellationToken>((u, _) => savedUser = u)
    .ReturnsAsync((User u, CancellationToken _) => u);

// Verification
repo.Verify(r => r.SaveAsync(It.IsAny<User>(), default), Times.Once);
repo.Verify(r => r.DeleteAsync(It.IsAny<int>(), default), Times.Never);
repo.VerifyAll();   // MockBehavior.Strict enforces no unexpected calls

Use MockBehavior.Strict for new tests — it forces you to set up all calls explicitly, making unexpected dependencies visible.

Do not mock value objects, records, or simple DTOs. Only mock boundaries: repositories, HTTP clients, external services, the clock (IDateTimeProvider).

NSubstitute — alternative to Moq

Some projects prefer NSubstitute for its concise syntax:

var repo = Substitute.For<IUserRepository>();
repo.FindAsync(42, default).Returns(testUser);
repo.ExistsByEmailAsync(Arg.Any<string>(), default).Returns(false);

// Verification
await repo.Received(1).SaveAsync(Arg.Any<User>(), default);
await repo.DidNotReceive().DeleteAsync(Arg.Any<int>(), default);

Match the mocking framework that is already used in the project. Do not mix Moq and NSubstitute in the same test project.

Coverage — coverlet + threshold

Measure coverage when running tests:

# Run tests with coverage
dotnet test --collect:"XPlat Code Coverage"

# Generate HTML report (requires reportgenerator global tool)
dotnet tool install --global dotnet-reportgenerator-globaltool
reportgenerator -reports:**/coverage.cobertura.xml -targetdir:coverage -reporttypes:Html

# Open report
open coverage/index.html     # macOS
start coverage/index.html    # Windows

Enforce a minimum coverage threshold in CI:

<!-- .csproj — dotnet test fails if line coverage < 80% -->
<PropertyGroup>
  <CollectCoverage>true</CollectCoverage>
  <CoverletOutputFormat>cobertura</CoverletOutputFormat>
  <Threshold>80</Threshold>
  <ThresholdType>line</ThresholdType>
</PropertyGroup>

Target ≥ 80% line coverage on business logic (services, domain objects, validators). Exclude framework glue code (Program.cs, Startup, configuration classes, migrations) using [ExcludeFromCodeCoverage] or coverlet filters.

Test organisation conventions

MyApp.sln
├── src/
│   └── MyApp/
│       ├── Users/
│       │   ├── User.cs
│       │   ├── IUserRepository.cs
│       │   └── UserService.cs
│       └── ...
└── tests/
    ├── MyApp.UnitTests/            # Unit tests — no external dependencies
    │   └── Users/
    │       └── UserServiceTests.cs
    └── MyApp.IntegrationTests/     # Integration tests — real DB, HTTP
        └── Users/
            └── UserEndpointsTests.cs

Naming: Unit test classes: {Subject}Tests. Integration test classes: {Subject}IntegrationTests or {Subject}Tests in the .IntegrationTests project.

Mirror the main project's namespace structure: MyApp.Users.UserServiceTests tests MyApp.Users.UserService.

IClassFixture — reuse expensive resources

// Shared fixture — created once per test class, disposed after all tests
public class DatabaseFixture : IDisposable
{
    public AppDbContext Db { get; }

    public DatabaseFixture()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;
        Db = new AppDbContext(options);
    }

    public void Dispose() => Db.Dispose();
}

public class UserRepositoryTests : IClassFixture<DatabaseFixture>
{
    private readonly AppDbContext _db;

    public UserRepositoryTests(DatabaseFixture fixture) => _db = fixture.Db;

    [Fact]
    public async Task SaveAndFind_RoundTrip()
    {
        var repo = new UserRepository(_db);
        var user = new User { Email = "alice@example.com" };

        await repo.SaveAsync(user, default);
        var found = await repo.FindAsync(user.Id, default);

        found.Should().NotBeNull();
        found!.Email.Should().Be("alice@example.com");
    }
}

For ASP.NET Core HTTP integration tests using WebApplicationFactory<TProgram>, see aspnet-core-plugin:aspnet-conventions.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Angular 18-21 project structure, standalone components vs NgModule, control flow (@if/@for/@switch + *ngIf/*ngFor legacy), decorators, dependency injection (inject() function), lifecycle hooks, pipes, Angular Universal SSR pointer. Use this skill to: - Detect project style (standalone vs NgModule) and apply matching patterns. - Pick correct decorators and DI approach. - Use modern control flow (@if/@for/@switch) in Angular 17+ projects. - Apply `inject()` function over constructor injection where appropriate. - Wire bootstrap correctly (bootstrapApplication for standalone, AppModule for legacy). Do NOT use this skill for: - State management (see angular-state-and-rx). - Routing (see angular-routing). - Forms (see angular-forms). - Testing (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

Angular forms: Reactive Forms (preferred — typed FormGroup/FormControl since Angular 14, FormBuilder, custom + async validators, FormArray, multi-step) and Template-driven (`[(ngModel)]` + FormsModule). Validation strategies, server error mapping, accessibility. Use this skill to: - Build Reactive Forms with typed FormGroup/FormControl. - Use FormBuilder для concise syntax. - Implement custom synchronous and async validators. - Wire FormArray for dynamic field lists. - Map server errors back to form fields. - Pick Reactive vs Template-driven (prefer Reactive). Do NOT use this skill for: - General conventions (see angular-conventions). - State management beyond forms (see angular-state-and-rx). - Routing (see angular-routing). - Testing forms (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

Angular Router (built-in `@angular/router`) — route configuration for standalone and NgModule projects, functional guards (Angular 14.1+), lazy loading, route resolvers, typed params via signals/observables, programmatic navigation, route data and meta. Use this skill to: - Configure routes (standalone-style or NgModule-style). - Use functional guards (canActivate as function, preferred over class-based in 17+). - Lazy-load components or feature modules. - Implement auth guards via route meta + functional guards. - Read params/queries via `inject(ActivatedRoute)` + signals or RxJS. Do NOT use this skill for: - General conventions (see angular-conventions). - State management (see angular-state-and-rx). - Forms (see angular-forms). - Testing routes (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

State management for Angular 18-21: signals (signal/computed/effect), services-as-state, NgRx Store + Effects + Selectors, NgRx Component Store, NgRx Signals (newer signal-based store). RxJS essentials — operators, async pipe, takeUntilDestroyed, signal/observable interop. Use this skill to: - Pick the right state tool (signals / services / NgRx variant / vue-query equivalent). - Use signals correctly (signal/computed/effect — when each). - Build a Pinia-style service-as-state singleton. - Set up NgRx Store + Effects + Selectors. - Use RxJS without leaking subscriptions (async pipe, takeUntilDestroyed, Subject patterns). - Bridge signals ↔ observables via toSignal / toObservable. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing state (see angular-routing). - Form state (see angular-forms). - Testing state (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

Testing Angular 18-21: TestBed, component harnesses (@angular/cdk/testing), Karma+Jasmine (default historical) vs Jest (jest-preset-angular, modern), Angular Testing Library (RTL-style). HttpClient mocking via HttpTestingController. NgRx Effects testing. Cypress / Playwright e2e. Use this skill to: - Detect runner (Karma+Jasmine vs Jest) and configure correctly. - Write component tests with TestBed. - Use component harnesses for Material / custom UI components. - Mock HttpClient via provideHttpClientTesting + HttpTestingController. - Test signal-based inputs with componentRef.setInput(). - Test NgRx Effects with provideMockActions. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing patterns broadly (see angular-routing — covers testing routes briefly). - Form patterns broadly (see angular-forms).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

Shared conventions for every SDLC development-phase architect agent: hard rules, code quality bar, workflow steps (superpowers invocation, spec reading, codebase exploration, verification), and the report/compact-summary contract. Architects load this skill first, then apply their stack-specific instructions on top.

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

See all skills by AratKruglik

Report a problem with this skill