.claude/skills/integration-testing-webapplicationfactory/SKILL.md
Use when writing integration tests for Razor Pages, MVC, or Minimal API applications to validate routing, middleware, page rendering, and HTTP behavior without a browser or live server, or when adding fast smoke tests to a CI pipeline. Covers WebApplicationFactory<Program> setup with public partial class Program, in-memory test server, AngleSharp HTML parsing, CSS selector assertions, redirect and status code testing, and a shared static fixture pattern for minimal per-test startup overhead. Domain: Testing, ASP.NET Core. Level: Intermediate. Tags: integration-testing, webapplicationfactory, razor-pages, anglesharp, http-testing.
npx skillsauth add klod68/littlerae integration-testing-webapplicationfactoryInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You need to test that your ASP.NET Core web application (Razor Pages, MVC, or Minimal API) works correctly end-to-end — HTTP pipeline, middleware, routing, page rendering — but without launching a browser or a real server.
Use WebApplicationFactory<Program> to spin up an in-memory test server, send HTTP requests via HttpClient, and parse HTML responses with AngleSharp. No browser, no ports, no flakiness.
| Aspect | WebApplicationFactory | Browser E2E (Playwright/Selenium) | |--------|----------------------|----------------------------------| | Speed | ~ms per test | ~seconds per test | | Stability | Deterministic | Flaky (timeouts, rendering) | | Setup | NuGet packages only | Browser install + drivers | | Scope | HTTP pipeline + HTML | Full rendering + JavaScript | | CI cost | Minimal | Heavy (headless browser) |
Use both: WebApplicationFactory for 80% of tests; browser E2E for JavaScript-dependent flows.
Program for the test hostIn the web app's Program.cs, add at the bottom:
// Expose Program class for WebApplicationFactory in tests
public partial class Program { }
using Microsoft.AspNetCore.Mvc.Testing;
/// <summary>
/// Shared WebApplicationFactory fixture. Created once, reused across all test classes.
/// </summary>
public static class WebAppFixture
{
private static readonly WebApplicationFactory<Program> _factory = new();
public static HttpClient Client { get; } = _factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false // Test redirects explicitly
});
}
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using System.Net;
/// <summary>
/// Base class for integration tests. Provides HTTP + HTML parsing helpers.
/// </summary>
public abstract class IntegrationTestBase
{
protected HttpClient Client => WebAppFixture.Client;
protected IHtmlParser HtmlParser { get; } = new HtmlParser();
/// <summary>
/// GET a page and parse the HTML response.
/// </summary>
protected async Task<IHtmlDocument> GetPageAsync(string relativePath)
{
var response = await Client.GetAsync(relativePath);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return (IHtmlDocument)await HtmlParser.ParseDocumentAsync(content);
}
/// <summary>
/// Assert that a page returns HTTP 200.
/// </summary>
protected async Task AssertPageLoadsAsync(string relativePath)
{
var response = await Client.GetAsync(relativePath);
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode,
$"Page {relativePath} returned {response.StatusCode}");
}
/// <summary>
/// Assert that a page body contains specific text.
/// </summary>
protected async Task AssertPageContainsTextAsync(string relativePath, string expectedText)
{
var document = await GetPageAsync(relativePath);
var bodyText = document.Body?.TextContent ?? string.Empty;
Assert.IsTrue(bodyText.Contains(expectedText),
$"Page {relativePath} does not contain '{expectedText}'");
}
/// <summary>
/// Assert that a CSS selector matches at least one element.
/// </summary>
protected async Task AssertElementExistsAsync(string relativePath, string cssSelector)
{
var document = await GetPageAsync(relativePath);
var element = document.QuerySelector(cssSelector);
Assert.IsNotNull(element,
$"Page {relativePath} has no element matching '{cssSelector}'");
}
/// <summary>
/// Assert a page returns the expected status code (for error pages, redirects).
/// </summary>
protected async Task AssertStatusCodeAsync(string relativePath, HttpStatusCode expected)
{
var response = await Client.GetAsync(relativePath);
Assert.AreEqual(expected, response.StatusCode);
}
}
[TestClass]
public class HomePageTests : IntegrationTestBase
{
[TestMethod]
public async Task HomePage_Returns200()
{
await AssertPageLoadsAsync("/");
}
[TestMethod]
public async Task HomePage_ContainsSiteTitle()
{
await AssertPageContainsTextAsync("/", "My Application");
}
[TestMethod]
public async Task HomePage_HasNavigationMenu()
{
await AssertElementExistsAsync("/", "nav");
}
[TestMethod]
public async Task NotFoundPage_Returns404()
{
await AssertStatusCodeAsync("/nonexistent-page", HttpStatusCode.NotFound);
}
}
.csproj)<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />
<PackageReference Include="AngleSharp" Version="1.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.x.x" />
<!-- Plus your test framework: MSTest, xUnit, or NUnit -->
</ItemGroup>
| What to Test | Helper | Example |
|-------------|--------|---------|
| Page loads | AssertPageLoadsAsync | All pages return 200 |
| Text content | AssertPageContainsTextAsync | Page has expected heading |
| HTML structure | AssertElementExistsAsync | Page has <nav>, <footer> |
| Status codes | AssertStatusCodeAsync | 404 for missing pages |
| Redirect | Check Location header | Login redirects to dashboard |
| API response | GetAsync + deserialize | JSON endpoint returns expected shape |
public partial class Program { } is required — without it, WebApplicationFactory<Program> can't find the entry point.AllowAutoRedirect = false is recommended — otherwise redirects are silently followed and you can't test redirect behavior.Development. Override with _factory.WithWebHostBuilder(b => b.UseEnvironment("Production")) if needed.Integration)/health endpoint)tools
Use when cross-cutting concerns (logging, metrics, validation, authorization) are tangled into command handlers or service methods, when building database command pipelines with reorderable concerns, or when HTTP client pipelines or message handlers need composable, independently-replaceable processing stages. Covers ICommandInterceptor interface, InterceptorPipeline with reverse-chain construction, zero-cost Empty sentinel to skip overhead when no interceptors are registered, and ConfigureAwait(false) discipline for library code. Domain: Architecture, Cross-Cutting Concerns. Level: Intermediate. Tags: interceptor, pipeline, middleware, decorator, cross-cutting-concerns.
development
Use when designing indexes for new tables, diagnosing slow queries that are not using indexes efficiently, reviewing index fragmentation and maintenance, or when the current indexing strategy results in key lookups, table scans, or missing index warnings. Covers clustered index key selection (narrow, unique, ever-increasing), non-clustered index design for query patterns, covering indexes with INCLUDE columns, filtered indexes for subset queries, composite index column ordering, DMV-based monitoring for missing and unused indexes, and rebuild vs reorganize maintenance thresholds. Domain: Database, Performance. Level: Intermediate. Tags: index, sql-server, covering-index, filtered-index, performance, dmv, maintenance.
development
Use when building a searchable in-memory catalog or registry for documentation sites, admin panels, or type/API browsers where you need keyword matching, fuzzy search, and ranked results without an external search engine or database. Covers RegistryService with weighted scoring across name, description, keywords, and method names; Levenshtein fuzzy matching; synonym expansion; category and subcategory filtering; and singleton DI registration for datasets of hundreds to low thousands of items. Domain: Search, Data Access Patterns. Level: Intermediate. Tags: search, registry, fuzzy-matching, in-memory, catalog, filtering.
tools
Use when adding database persistence to a Truenorth.Components.* library, migrating legacy persistence code to the standard pattern, or adding new entities to an existing component following the stored-procedure-only architecture with EXECUTE-only permissions. Covers the 8-artifact pattern: embedded settings, language constants, static repository, component factory, DbMappings, Insert/Update/Delete commands, SelectAll/SelectById/Exists queries, and a CrudPersistenceHelper-based persistence service with IList/IListCollectable/ITableCollectable output variants. Domain: Data Access / Persistence. Level: Advanced. Tags: persistence, stored-procedure, CRUD, ADO.NET, provider-agnostic.