ronnythedev/dotnet-clean-architecture-skills/22-dotnet-integration-testing

Claude Code and agent skill by ronnythedev/dotnet-clean-architecture-skills

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Configures integration tests with WebApplicationFactory and Testcontainers. Provides test database setup, authentication helpers, and utilities for testing API endpoints with real dependencies.

Install

npx vskill@latest install ronnythedev/dotnet-clean-architecture-skills/22-dotnet-integration-testing

SKILL.md

Integration Test Setup

Overview

Integration tests verify the full request pipeline:

  • WebApplicationFactory - In-memory test server
  • Testcontainers - Real PostgreSQL in Docker
  • Respawn - Fast database cleanup between tests
  • Authentication helpers - Test with different users/roles

Quick Reference

ComponentPurpose
IntegrationTestWebAppFactoryCustom test server factory
BaseIntegrationTestBase class for all tests
RespawnerDatabase cleanup utility
TestAuthHandlerFake authentication handler

Test Project Structure

tests/
└── {name}.Api.IntegrationTests/
├── Infrastructure/
│ ├── IntegrationTestWebAppFactory.cs
│ ├── BaseIntegrationTest.cs
│ ├── TestAuthHandler.cs
│ └── FakeUserContext.cs
├── {Feature}/
│ ├── Create{Entity}Tests.cs
│ └── Get{Entity}Tests.cs
└── {name}.Api.IntegrationTests.csproj

Template: Test Project File

xml
<!-- tests/{name}.Api.IntegrationTests/{name}.Api.IntegrationTests.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Respawn" Version="6.1.0" />
<PackageReference Include="Testcontainers.PostgreSql" Version="3.6.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\{name}.api\{name}.api.csproj" />
<ProjectReference Include="..\..\src\{name}.infrastructure\{name}.infrastructure.csproj" />
</ItemGroup>
</Project>

Template: WebApplicationFactory

csharp
// tests/{name}.Api.IntegrationTests/Infrastructure/IntegrationTestWebAppFactory.cs
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Testcontainers.PostgreSql;
using {name}.infrastructure;
namespace {name}.Api.IntegrationTests.Infrastructure;
public class IntegrationTestWebAppFactory
: WebApplicationFactory<Program>, IAsyncLifetime
{
private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder()
.WithImage("postgres:15-alpine")
.WithDatabase("testdb")
.WithUsername("postgres")
.WithPassword("postgres")
.Build();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
// ═══════════════════════════════════════════════════════════════
// REPLACE DATABASE WITH TEST CONTAINER
// ═══════════════════════════════════════════════════════════════
services.RemoveAll(typeof(DbContextOptions<ApplicationDbContext>));
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseNpgsql(_dbContainer.GetConnectionString());
});
// ═══════════════════════════════════════════════════════════════
// REPLACE AUTHENTICATION WITH TEST HANDLER
// ═══════════════════════════════════════════════════════════════
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName;
options.DefaultChallengeScheme = TestAuthHandler.SchemeName;
})
.AddScheme<TestAuthSchemeOptions, TestAuthHandler>(
TestAuthHandler.SchemeName,
options => { });
// ═══════════════════════════════════════════════════════════════
// REPLACE EXTERNAL SERVICES WITH FAKES
// ═══════════════════════════════════════════════════════════════
// services.RemoveAll<IEmailService>();
// services.AddSingleton<IEmailService, FakeEmailService>();
});
builder.UseEnvironment("Testing");
}
public async Task InitializeAsync()
{
await _dbContainer.StartAsync();
// Apply migrations
using var scope = Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await dbContext.Database.MigrateAsync();
}
public new async Task DisposeAsync()
{
await _dbContainer.StopAsync();
}
}

Template: Test Authentication Handler

csharp
// tests/{name}.Api.IntegrationTests/Infrastructure/TestAuthHandler.cs
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace {name}.Api.IntegrationTests.Infrastructure;
public class TestAuthSchemeOptions : AuthenticationSchemeOptions
{
public Guid? UserId { get; set; }
public string? Email { get; set; }
public string[]? Roles { get; set; }
public string[]? Permissions { get; set; }
}
public class TestAuthHandler : AuthenticationHandler<TestAuthSchemeOptions>
{
public const string SchemeName = "TestScheme";
public const string TestUserIdHeader = "X-Test-User-Id";
public const string TestUserEmailHeader = "X-Test-User-Email";
public const string TestUserRolesHeader = "X-Test-User-Roles";
public const string TestUserPermissionsHeader = "X-Test-User-Permissions";
public TestAuthHandler(
IOptionsMonitor<TestAuthSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Check for test headers
if (!Request.Headers.TryGetValue(TestUserIdHeader, out var userIdHeader))
{
return Task.FromResult(AuthenticateResult.NoResult());
}
if (!Guid.TryParse(userIdHeader, out var userId))
{
return Task.FromResult(AuthenticateResult.Fail("Invalid user ID"));
}
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, userId.ToString()),
new("sub", userId.ToString())
};
// Add email
if (Request.Headers.TryGetValue(TestUserEmailHeader, out var emailHeader))
{
claims.Add(new Claim(ClaimTypes.Email, emailHeader.ToString()));
claims.Add(new Claim("email", emailHeader.ToString()));
}
// Add roles
if (Request.Headers.TryGetValue(TestUserRolesHeader, out var rolesHeader))
{
foreach (var role in rolesHeader.ToString().Split(','))
{
claims.Add(new Claim(ClaimTypes.Role, role.Trim()));
}
}
// Add permissions
if (Request.Headers.TryGetValue(TestUserPermissionsHeader, out var permissionsHeader))
{
foreach (var permission in permissionsHeader.ToString().Split(','))
{
claims.Add(new Claim("permission", permission.Trim()));
}
}
var identity = new ClaimsIdentity(claims, SchemeName);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, SchemeName);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}

Template: Base Integration Test

csharp
// tests/{name}.Api.IntegrationTests/Infrastructure/BaseIntegrationTest.cs
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Npgsql;
using Respawn;
using {name}.infrastructure;
namespace {name}.Api.IntegrationTests.Infrastructure;
[Collection("Integration")]
public abstract class BaseIntegrationTest : IAsyncLifetime
{
protected readonly IntegrationTestWebAppFactory Factory;
protected readonly HttpClient Client;
protected readonly IServiceScope Scope;
protected readonly ApplicationDbContext DbContext;
private static Respawner? _respawner;
private static string? _connectionString;
protected BaseIntegrationTest(IntegrationTestWebAppFactory factory)
{
Factory = factory;
Client = factory.CreateClient();
Scope = factory.Services.CreateScope();
DbContext = Scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
}
// ═══════════════════════════════════════════════════════════════
// AUTHENTICATION HELPERS
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Configure client to authenticate as a specific user
/// </summary>
protected void AuthenticateAs(
Guid userId,
string email = "test@example.com",
string[]? roles = null,
string[]? permissions = null)
{
Client.DefaultRequestHeaders.Add(TestAuthHandler.TestUserIdHeader, userId.ToString());
Client.DefaultRequestHeaders.Add(TestAuthHandler.TestUserEmailHeader, email);
if (roles?.Length > 0)
{
Client.DefaultRequestHeaders.Add(
TestAuthHandler.TestUserRolesHeader,
string.Join(",", roles));
}
if (permissions?.Length > 0)
{
Client.DefaultRequestHeaders.Add(
TestAuthHandler.TestUserPermissionsHeader,
string.Join(",", permissions));
}
}
/// <summary>
/// Configure client to authenticate as admin
/// </summary>
protected void AuthenticateAsAdmin()
{
AuthenticateAs(
userId: Guid.NewGuid(),
email: "admin@example.com",
roles: new[] { "Admin" },
permissions: new[] { "users:read", "users:write", "users:delete" });
}
/// <summary>
/// Remove authentication headers
/// </summary>
protected void RemoveAuthentication()
{
Client.DefaultRequestHeaders.Remove(TestAuthHandler.TestUserIdHeader);
Client.DefaultRequestHeaders.Remove(TestAuthHandler.TestUserEmailHeader);
Client.DefaultRequestHeaders.Remove(TestAuthHandler.TestUserRolesHeader);
Client.DefaultRequestHeaders.Remove(TestAuthHandler.TestUserPermissionsHeader);
}
// ═══════════════════════════════════════════════════════════════
// HTTP HELPERS
// ═══════════════════════════════════════════════════════════════
protected async Task<HttpResponseMessage> GetAsync(string url)
{
return await Client.GetAsync(url);
}
protected async Task<T?> GetAsync<T>(string url)
{
var response = await Client.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<T>();
}
protected async Task<HttpResponseMessage> PostAsync<T>(string url, T content)
{
return await Client.PostAsJsonAsync(url, content);
}
protected async Task<TResponse?> PostAsync<TRequest, TResponse>(string url, TRequest content)
{
var response = await Client.PostAsJsonAsync(url, content);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<TResponse>();
}

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
64
Repository forks
11
7-day trend score
21

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/ronnythedev/dotnet-clean-architecture-skills/22-dotnet-integration-testing/badge)](https://verified-skill.com/skills/ronnythedev/dotnet-clean-architecture-skills/22-dotnet-integration-testing)

Something wrong with this skill or its page? Tell us.