testing · codewithmukesh — скилл для Claude Code: тесты .NET 10
Скилл задаёт стратегию тестирования приложений .NET 10: интеграционные тесты через WebApplicationFactory, реальные базы в Testcontainers, снапшоты Verify.
npx skills add codewithmukesh/dotnet-claude-kit --skill testing -a claude-codeЧто делает testing
Скилл включается, когда вы пишете тесты, настраиваете тестовую инфраструктуру или проверяете покрытие. Внутри — набор шаблонов под .NET 10: базовые тесты на xUnit v3, интеграционные через WebApplicationFactory<Program>, Testcontainers для PostgreSQL, SQL Server и Redis, снапшот-тесты Verify, билдеры тестовых данных и работа с FakeTimeProvider из Microsoft.Extensions.TimeProvider.Testing.
Главная мысль, которую он проводит: интеграционный тест на WebApplicationFactory ценнее юнит-теста, потому что разом проверяет маршрутизацию, биндинг, валидацию, логику и сохранение. В примерах показано, как подменить реальную базу на контейнер и прогнать миграции в InitializeAsync. Схема AAA (Arrange, Act, Assert) объявлена обязательной, а тестировать советуется поведение, а не реализацию.
Учтите: примеры написаны под xUnit v3, где IAsyncLifetime.InitializeAsync возвращает ValueTask, а не Task, как в v2. Если проект ещё на xUnit v2, сигнатуры придётся переписать. Для Testcontainers нужен запущенный Docker — без него контейнеры с базой не поднимутся.
Какие задачи решает
- написать интеграционный тест на весь HTTP-конвейер
- поднять реальную PostgreSQL или SQL Server в тесте
- сравнить ответ API со снапшотом вместо ручных проверок
- собрать тестовые данные билдером без дублирования кода
- протестировать код, зависящий от текущего времени
Примеры запросов агенту
- напиши интеграционный тест для POST /api/orders через WebApplicationFactory
- настрой Testcontainers с PostgreSQL для тестов
- добавь снапшот-тест на ответ GetOrder через Verify
Как установить testing
1Через Skills CLI (Vercel)
npx skills add codewithmukesh/dotnet-claude-kit --skill testing -a claude-code2Вручную: положить файлы
~/.claude/skills/testing/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/testing/
1Через Skills CLI (Vercel)
npx skills add codewithmukesh/dotnet-claude-kit --skill testing -a codex2Вручную: положить файлы
.agents/skills/testing/SKILL.mdДля всех проектов — ~/.agents/skills/
1Через Skills CLI (Vercel)
npx skills add codewithmukesh/dotnet-claude-kit --skill testing -a cursor2Вручную: положить файлы
.cursor/skills/testing/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
1GitHub CLI
gh skill install codewithmukesh/dotnet-claude-kit testing2Вручную: положить файлы
.github/skills/testing/SKILL.md1Через Skills CLI (Vercel)
npx skills add codewithmukesh/dotnet-claude-kit --skill testing -a gemini-cli2Командой платформы
gemini skills install https://github.com/codewithmukesh/dotnet-claude-kit --path skills/testingВручную: положить файлы
.devin/skills/testing/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Через Skills CLI (Vercel)
npx skills add codewithmukesh/dotnet-claude-kit --skill testing -a opencodeClaude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов
Из того же набора46
Другие версии testing
Похожие
Рядом в теме «Тесты и QA»
Всё по теме «Тесты и QA» →Вопросы о testing
Как установить testing в Claude?
Выполните npx skills add codewithmukesh/dotnet-claude-kit --skill testing -a claude-code. После этого файл скилла окажется в ~/.claude/skills/testing/SKILL.md.
Что делает скилл testing?
Описывает стратегию тестирования для .NET 10: xUnit v3, интеграционные тесты через WebApplicationFactory, Testcontainers для реальных баз, снапшоты Verify и схему AAA. Агент подхватывает его, когда речь заходит о тестах или покрытии.
Какие инструменты нужны для работы скилла?
Примеры опираются на xUnit v3, Testcontainers и Verify, а для тестов времени — на FakeTimeProvider из Microsoft.Extensions.TimeProvider.Testing. Для Testcontainers понадобится Docker.
Исходный текст
SKILL.md · MIT
---
name: testing
description: >
Testing strategy for .NET 10 applications. Covers xUnit v3, WebApplicationFactory
for integration tests, Testcontainers for real database testing, Verify for
snapshot testing, and the AAA pattern.
Load this skill when writing tests, setting up test infrastructure, reviewing
test coverage, or when the user mentions "test", "xUnit", "WebApplicationFactory",
"Testcontainers", "integration test", "unit test", "bUnit", "snapshot test",
"Verify", "test coverage", "AAA pattern", "WireMock", or "FakeTimeProvider".
---
# Testing (.NET 10)
## Core Principles
1. **Integration tests are the highest-value tests** — A single `WebApplicationFactory` test covers routing, binding, validation, business logic, and persistence in one shot. Start here before writing unit tests.
2. **Real databases in tests** — Use Testcontainers to spin up real PostgreSQL/SQL Server instances. In-memory providers hide real bugs (transactions, constraints, SQL generation).
3. **AAA pattern is mandatory** — Every test has three clearly separated sections: Arrange, Act, Assert. No mixing.
4. **Test behavior, not implementation** — Tests should survive refactoring. Test what the system does, not how it does it.
## Patterns
### xUnit v3 Basics
```csharp
public class OrderServiceTests
{
[Fact]
public async Task CreateOrder_WithValidItems_ReturnsSuccessResult()
{
// Arrange
var db = CreateInMemoryDb();
var clock = new FakeTimeProvider(new DateTimeOffset(2025, 1, 15, 0, 0, 0, TimeSpan.Zero));
var service = new OrderService(db, clock);
var request = new CreateOrderRequest("customer-1", [new("product-1", 2)]);
// Act
var result = await service.CreateAsync(request);
// Assert
Assert.True(result.IsSuccess);
Assert.NotEqual(Guid.Empty, result.Value.Id);
Assert.Equal(clock.GetUtcNow(), result.Value.CreatedAt);
}
[Theory]
[InlineData("")]
[InlineData(null)]
public async Task CreateOrder_WithInvalidCustomerId_ReturnsFailure(string? customerId)
{
// Arrange
var service = CreateService();
// Act
var result = await service.CreateAsync(new CreateOrderRequest(customerId!, []));
// Assert
Assert.False(result.IsSuccess);
}
}
```
### Integration Tests with WebApplicationFactory
The highest-value test pattern. Tests the full HTTP pipeline.
```csharp
// Fixtures/ApiFixture.cs
public class ApiFixture : WebApplicationFactory<Program>, IAsyncLifetime
{
private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
.WithImage("postgres:18")
.Build();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Replace the real DB with Testcontainers
services.RemoveAll<DbContextOptions<AppDbContext>>();
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(_postgres.GetConnectionString()));
});
}
// xUnit v3: IAsyncLifetime.InitializeAsync returns ValueTask (v2 used Task)
public async ValueTask InitializeAsync()
{
await _postgres.StartAsync();
// Apply migrations
using var scope = Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Database.MigrateAsync();
}
// xUnit v3: IAsyncLifetime inherits IAsyncDisposable — override the
// ValueTask DisposeAsync that WebApplicationFactory already provides
public override async ValueTask DisposeAsync()
{
await _postgres.DisposeAsync();
await base.DisposeAsync();
}
}
```
```csharp
// Tests/Orders/CreateOrderTests.cs
public class CreateOrderTests(ApiFixture fixture) : IClassFixture<ApiFixture>
{
private readonly HttpClient _client = fixture.CreateClient();
[Fact]
public async Task CreateOrder_ReturnsCreated_WithValidRequest()
{
// Arrange
var request = new CreateOrderRequest("customer-1", [new("product-1", 2)]);
// Act
var response = await _client.PostAsJsonAsync("/api/orders", request);
// Assert
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var order = await response.Content.ReadFromJsonAsync<OrderResponse>();
Assert.NotNull(order);
Assert.NotEqual(Guid.Empty, order.Id);
Assert.Contains("/api/orders/", response.Headers.Location?.ToString());
}
[Fact]
public async Task CreateOrder_ReturnsValidationProblem_WithEmptyItems()
{
// Arrange
var request = new CreateOrderRequest("customer-1", []);
// Act
var response = await _client.PostAsJsonAsync("/api/orders", request);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
}
```
### Testcontainers for Real Database Testing
```csharp
// For SQL Server
private readonly MsSqlContainer _mssql = new MsSqlBuilder()
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
.Build();
// For PostgreSQL
private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
.WithImage("postgres:18")
.Build();
// For Redis
private readonly RedisContainer _redis = new RedisBuilder()
.WithImage("redis:7")
.Build();
```
### Verify Snapshot Testing
Use Verify for complex response objects where manual assertions would be fragile.
```csharp
[Fact]
public async Task GetOrder_MatchesSnapshot()
{
// Arrange
await SeedOrder(fixture);
// Act
var response = await _client.GetAsync("/api/orders/known-id");
var content = await response.Content.ReadAsStringAsync();
// Assert — compares against a stored .verified.txt file
await Verify(content);
}
```
On first run, Verify creates a `.verified.txt` file. On subsequent runs, it compares output. If the output changes, the test fails and shows a diff.
### Test Data Builders
```csharp
public class OrderBuilder
{
private string _customerId = "default-customer";
private List<OrderItem> _items = [new("product-1", 1, 9.99m)];
private OrderStatus _status = OrderStatus.Pending;
public OrderBuilder WithCustomer(string customerId)
{
_customerId = customerId;
return this;
}
public OrderBuilder WithItems(params OrderItem[] items)
{
_items = [..items];
return this;
}
public OrderBuilder WithStatus(OrderStatus status)
{
_status = status;
return this;
}
public Order Build() => Order.Create(_customerId, _items, _status);
}
// Usage in tests
var order = new OrderBuilder()
.WithCustomer("vip-customer")
.WithStatus(OrderStatus.Confirmed)
.Build();
```
### Testing Time-Dependent Code
Use `TimeProvider` (built into .NET 8+) and `FakeTimeProvider` from `Microsoft.Extensions.TimeProvider.Testing`.
```csharp
[Fact]
public async Task ExpireOrders_MarksOldPendingOrdersAsExpired()
{
// Arrange
var clock = new FakeTimeProvider(new DateTimeOffset(2025, 6, 1, 0, 0, 0, TimeSpan.Zero));
var db = CreateDb();
var order = Order.Create("customer-1", items, clock.GetUtcNow());
db.Orders.Add(order);
await db.SaveChangesAsync();
// Advance time past expiry threshold
clock.Advance(TimeSpan.FromDays(31));
var handler = new ExpireOrders.Handler(db, clock);
// Act
await handler.Handle(new ExpireOrders.Command(), CancellationToken.None);
// Assert
var updated = await db.Orders.FindAsync(order.Id);
Assert.Equal(OrderStatus.Expired, updated!.Status);
}
```
### Test Naming Convention
Use the pattern: `MethodName_StateUnderTest_ExpectedBehavior`
```csharp
[Fact] public async Task CreateOrder_WithValidItems_ReturnsSuccessResult() { }
[Fact] public async Task CreateOrder_WithEmptyItems_ReturnsValidationError() { }
[Fact] public async Task GetOrder_WithNonExistentId_ReturnsNotFound() { }
[Fact] public async Task CancelOrder_WhenAlreadyShipped_ReturnsConflict() { }
```
## Anti-patterns
### Don't Use In-Memory Database for Integration Tests
```csharp
// BAD — hides real SQL behavior, transactions, constraints
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("TestDb"));
// GOOD — Testcontainers with real database
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(testContainer.GetConnectionString()));
```
### Don't Test Implementation Details
```csharp
// BAD — testing that a specific repository method was called
mock.Verify(x => x.AddAsync(It.IsAny<Order>()), Times.Once);
mock.Verify(x => x.SaveChangesAsync(), Times.Once);
// GOOD — test the observable outcome
var order = await db.Orders.FindAsync(orderId);
Assert.NotNull(order);
Assert.Equal(OrderStatus.Created, order.Status);
```
### Don't Share Mutable State Between Tests
```csharp
// BAD — static shared state
private static readonly AppDbContext SharedDb = CreateDb();
// GOOD — fresh state per test (or use IAsyncLifetime for shared fixtures)
private AppDbContext CreateDb() => new(new DbContextOptionsBuilder<AppDbContext>()...);
```
### Don't Write Assertion-Free Tests
```csharp
// BAD — no assertion, only checks it doesn't throw
[Fact]
public async Task CreateOrder_Works()
{
await service.CreateAsync(request);
// "it didn't throw, so it works!" — NO
}
// GOOD — assert the expected outcome
[Fact]
public async Task CreateOrder_PersistsOrderToDatabase()
{
var result = await service.CreateAsync(request);
var persisted = await db.Orders.FindAsync(result.Value.Id);
Assert.NotNull(persisted);
Assert.Equal(request.CustomerId, persisted.CustomerId);
}
```
## Decision Guide
| Scenario | Recommendation |
|----------|---------------|
| Testing an API endpoint | `WebApplicationFactory` integration test |
| Testing business logic in isolation | Unit test with fakes/stubs |
| Database-dependent tests | Testcontainers (real DB) |
| Complex response validation | Verify snapshot testing |
| Time-dependent logic | `FakeTimeProvider` |
| External API dependency | `WireMock.Net` or `HttpMessageHandler` stub |
| Parameterized test cases | `[Theory]` with `[InlineData]` or `[MemberData]` |
| Test data setup | Builder pattern |
| Shared expensive fixture | `IClassFixture<T>` with `IAsyncLifetime` |
© codewithmukesh · MIT · codewithmukesh/dotnet-claude-kit