csharp-testing

C# testing patterns: xUnit with [Fact]/[Theory], FluentAssertions, Moq mocking, NSubstitute, WebApplicationFactory integration tests, Testcontainers for real DB, Bogus fake data generation, Respawn DB reset, Coverlet coverage. Use when writing or reviewing C# tests.

8 stars

Best use case

csharp-testing is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

C# testing patterns: xUnit with [Fact]/[Theory], FluentAssertions, Moq mocking, NSubstitute, WebApplicationFactory integration tests, Testcontainers for real DB, Bogus fake data generation, Respawn DB reset, Coverlet coverage. Use when writing or reviewing C# tests.

Teams using csharp-testing should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/csharp-testing/SKILL.md --create-dirs "https://raw.githubusercontent.com/marvinrichter/clarc/main/skills/csharp-testing/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/csharp-testing/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How csharp-testing Compares

Feature / Agentcsharp-testingStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

C# testing patterns: xUnit with [Fact]/[Theory], FluentAssertions, Moq mocking, NSubstitute, WebApplicationFactory integration tests, Testcontainers for real DB, Bogus fake data generation, Respawn DB reset, Coverlet coverage. Use when writing or reviewing C# tests.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# C# Testing

## When to Activate

- Writing C# unit or integration tests with xUnit
- Setting up FluentAssertions and Moq
- Writing WebApplicationFactory integration tests
- Using Testcontainers for database integration tests
- Generating realistic test data with Bogus instead of hand-crafted fixtures
- Enforcing a minimum code coverage threshold via Coverlet in CI
- Migrating from MSTest or NUnit to xUnit and needing idiomatic test structure

---

## xUnit — Unit Tests

```csharp
using FluentAssertions;
using Moq;
using Xunit;

public class UserServiceTests
{
    private readonly Mock<IUserRepository> _repo   = new(MockBehavior.Strict);
    private readonly Mock<IEmailService>   _email  = new(MockBehavior.Loose);
    private readonly Mock<ILogger<UserService>> _logger = new();
    private readonly UserService _sut;

    public UserServiceTests()
    {
        _sut = new UserService(_repo.Object, _email.Object, _logger.Object);
    }

    [Fact]
    public async Task RegisterAsync_CreatesUser_WhenEmailIsUnique()
    {
        // Arrange
        _repo.Setup(r => r.FindByEmailAsync("alice@example.com", default))
             .ReturnsAsync((User?)null);
        _repo.Setup(r => r.SaveAsync(It.IsAny<User>(), default))
             .Returns(Task.CompletedTask);

        // Act
        var user = await _sut.RegisterAsync(
            new RegisterUserCommand("Alice", "alice@example.com", "secure_pass_123"));

        // Assert
        user.Should().NotBeNull();
        user.Name.Should().Be("Alice");
        user.Email.Value.Should().Be("alice@example.com");
        _repo.VerifyAll();
    }

    [Fact]
    public async Task RegisterAsync_ThrowsDuplicateEmailException_WhenEmailTaken()
    {
        _repo.Setup(r => r.FindByEmailAsync("taken@example.com", default))
             .ReturnsAsync(new User(1, "Bob", new Email("taken@example.com"), DateTimeOffset.UtcNow));

        var act = async () => await _sut.RegisterAsync(
            new RegisterUserCommand("Alice", "taken@example.com", "pass"));

        await act.Should().ThrowAsync<DuplicateEmailException>()
            .WithMessage("*taken@example.com*");
    }

    [Theory]
    [InlineData("")]
    [InlineData(" ")]
    [InlineData(null)]
    public async Task RegisterAsync_ThrowsArgumentException_ForEmptyName(string? name)
    {
        var act = async () => await _sut.RegisterAsync(
            new RegisterUserCommand(name!, "alice@example.com", "pass"));

        await act.Should().ThrowAsync<ArgumentException>()
            .WithParameterName("Name");
    }
}
```

---

## FluentAssertions Cheat Sheet

```csharp
// Objects
result.Should().NotBeNull();
result.Should().BeOfType<User>();
result.Should().BeEquivalentTo(expected, opt => opt.ExcludingMissingMembers());

// Strings
name.Should().Be("Alice");
name.Should().StartWith("Al").And.EndWith("ce");
name.Should().Contain("li");
name.Should().MatchRegex(@"^[A-Z][a-z]+$");

// Numbers
count.Should().BeGreaterThan(0);
value.Should().BeInRange(1, 100);
price.Should().BeApproximately(9.99m, 0.01m);

// Collections
list.Should().HaveCount(3);
list.Should().Contain(u => u.Name == "Alice");
list.Should().BeInAscendingOrder(u => u.Name);
list.Should().OnlyContain(u => u.IsActive);
list.Should().BeEmpty();

// Booleans
flag.Should().BeTrue();
flag.Should().BeFalse();

// Exceptions (sync)
act.Should().Throw<ArgumentException>().WithMessage("*positive*");
act.Should().NotThrow();

// Exceptions (async)
await asyncAct.Should().ThrowAsync<InvalidOperationException>();
await asyncAct.Should().NotThrowAsync();

// Date/time
dto.CreatedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
```

---

## NSubstitute (alternative to Moq)

```csharp
using NSubstitute;

var repo = Substitute.For<IUserRepository>();
repo.FindByEmailAsync("alice@example.com", Arg.Any<CancellationToken>())
    .Returns(Task.FromResult<User?>(null));

var sut = new UserService(repo, /* ... */);

// Assert call received
await repo.Received(1).SaveAsync(Arg.Is<User>(u => u.Name == "Alice"), Arg.Any<CancellationToken>());
await repo.DidNotReceiveWithAnyArgs().RemoveAsync(default);
```

---

## WebApplicationFactory — Integration Tests

```csharp
public class UserApiTests : IClassFixture<ApiFactory>
{
    private readonly HttpClient _client;

    public UserApiTests(ApiFactory factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task PostUser_Returns201_WhenValid()
    {
        var response = await _client.PostAsJsonAsync("/users", new
        {
            name     = "Alice",
            email    = "alice@example.com",
            password = "super_secure_pass_12"
        });

        response.StatusCode.Should().Be(HttpStatusCode.Created);

        var body = await response.Content.ReadFromJsonAsync<UserDto>();
        body!.Name.Should().Be("Alice");
        response.Headers.Location.Should().NotBeNull();
    }
}

// Shared fixture — creates one server for all tests
public sealed class ApiFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            services.RemoveAll<DbContextOptions<AppDbContext>>();
            services.AddDbContext<AppDbContext>(o => o.UseInMemoryDatabase(Guid.NewGuid().ToString()));
        });
    }
}
```

---

## Testcontainers — Real Database Integration Tests

```csharp
public sealed class UserRepositoryTests : IAsyncLifetime
{
    private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
        .WithImage("postgres:16-alpine")
        .Build();

    private AppDbContext _db = null!;

    public async Task InitializeAsync()
    {
        await _postgres.StartAsync();

        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseNpgsql(_postgres.GetConnectionString())
            .Options;

        _db = new AppDbContext(options);
        await _db.Database.MigrateAsync();
    }

    public async Task DisposeAsync()
    {
        await _db.DisposeAsync();
        await _postgres.DisposeAsync();
    }

    [Fact]
    public async Task SaveAsync_PersistsUser()
    {
        var repo = new EfUserRepository(_db);
        var user = User.Create("Alice", new Email("alice@example.com"));

        await repo.SaveAsync(user, default);

        var found = await repo.FindByEmailAsync("alice@example.com", default);
        found.Should().NotBeNull();
        found!.Name.Should().Be("Alice");
    }
}
```

---

## Bogus — Fake Data Generation

```csharp
using Bogus;

public static class UserFaker
{
    private static readonly Faker<User> _faker = new Faker<User>()
        .RuleFor(u => u.Name,  f => f.Name.FullName())
        .RuleFor(u => u.Email, f => new Email(f.Internet.Email()))
        .RuleFor(u => u.CreatedAt, f => f.Date.RecentOffset(30));

    public static User Generate() => _faker.Generate();
    public static List<User> Generate(int count) => _faker.Generate(count);
}

// In tests
var users = UserFaker.Generate(20);
```

---

## Coverlet — Code Coverage

```bash
dotnet test --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura

# HTML report with ReportGenerator
reportgenerator -reports:"coverage.cobertura.xml" -targetdir:"coverage-report" -reporttypes:Html
```

`Directory.Build.props` to enforce minimum:

```xml
<PropertyGroup>
  <CollectCoverage>true</CollectCoverage>
  <CoverletOutputFormat>cobertura</CoverletOutputFormat>
  <Threshold>80</Threshold>
  <ThresholdType>line</ThresholdType>
</PropertyGroup>
```

Related Skills

visual-testing

8
from marvinrichter/clarc

Visual Regression Testing: tool comparison (Chromatic/Percy/Playwright screenshots/BackstopJS), pixel-diff vs AI-based comparison, baseline management, flakiness strategies (masks, tolerances, waitForLoadState), CI integration with GitHub Actions, and Storybook integration.

typescript-testing

8
from marvinrichter/clarc

TypeScript testing patterns: Vitest for unit/integration, Playwright for E2E, MSW for API mocking, Testing Library for React components. Core TDD methodology for TypeScript/JavaScript projects.

swift-testing

8
from marvinrichter/clarc

Swift testing patterns: Swift Testing framework (Swift 6+), XCTest for UI tests, async/await test cases, actor testing, Combine testing, and XCUITest for UI automation. TDD for Swift/SwiftUI.

swift-protocol-di-testing

8
from marvinrichter/clarc

Protocol-based dependency injection for testable Swift code — mock file system, network, and external APIs using focused protocols and Swift Testing.

scala-testing

8
from marvinrichter/clarc

Scala testing with ScalaTest, MUnit, and ScalaCheck: FunSpec/FlatSpec test structure, property-based testing with forAll, mocking with MockitoSugar, Cats Effect testing with munit-cats-effect (runTest/IOSuite), ZIO Test, Testcontainers-Scala for database integration tests, and CI integration with sbt. Use when writing or reviewing Scala tests.

rust-testing

8
from marvinrichter/clarc

Rust testing patterns — unit tests with mockall, integration tests with sqlx transactions, HTTP handler testing (axum), benchmarks (criterion), property tests (proptest), fuzzing, and CI with cargo-nextest.

rust-testing-advanced

8
from marvinrichter/clarc

Advanced Rust testing anti-patterns and corrections — cfg(test) placement, expect() over unwrap(), mockall expectation ordering, executor mixing (#[tokio::test] vs block_on), PgPool isolation with

ruby-testing

8
from marvinrichter/clarc

RSpec testing patterns for Ruby and Rails — factories, mocks, request specs, feature specs, VCR, and SimpleCov coverage.

r-testing

8
from marvinrichter/clarc

R testing patterns: testthat 3e with expect_* assertions, snapshot testing, mocking with mockery and httptest2, covr code coverage, lintr static analysis, property-based testing with hedgehog, testing Shiny apps with shinytest2. Use when writing or reviewing R tests.

python-testing

8
from marvinrichter/clarc

Python testing strategies using pytest, TDD methodology, fixtures, mocking, and parametrization. Core testing fundamentals.

python-testing-advanced

8
from marvinrichter/clarc

Advanced Python testing — async testing with pytest-asyncio, exception/side-effect testing, test organization, common patterns (API, database, class methods), pytest configuration, and CLI reference. Extends python-testing.

php-testing

8
from marvinrichter/clarc

PHP testing patterns: PHPUnit 11 with mocks and data providers, Pest v3 with expectations and datasets, Laravel feature/HTTP tests with RefreshDatabase, Symfony WebTestCase, PHPStan static analysis, Infection mutation testing. Use when writing or reviewing PHP tests.