Skip to content

tests: migrate integration tests to TUnit.AspNetCore WebApplicationTest#1100

Open
BenjaminMichaelis wants to merge 5 commits into
mainfrom
agents/tunit-aspnetcore-integration-tests
Open

tests: migrate integration tests to TUnit.AspNetCore WebApplicationTest#1100
BenjaminMichaelis wants to merge 5 commits into
mainfrom
agents/tunit-aspnetcore-integration-tests

Conversation

@BenjaminMichaelis
Copy link
Copy Markdown
Member

Summary

Migrates EssentialCSharp.Web.Tests from a hand-rolled WebApplicationFactory<Program> + IAsyncInitializer setup to TUnit.AspNetCore's first-class WebApplicationTest<TFactory, TEntryPoint> pattern.

All 130 tests pass.


Changes

New package

  • Added TUnit.AspNetCore 1.40.5 to Directory.Packages.props and the test .csproj

WebApplicationFactory.cs

  • Base class: WebApplicationFactory<Program>TestWebApplicationFactory<Program>
    • Gains trace correlation, per-test logging, TestContext.Current in request handlers
    • Fixes TUnit0064 analyzer warning
  • Removed IAsyncInitializer — TUnit manages factory lifecycle
  • Added ConcurrentBag<SqliteConnection> for thread-safe connection tracking
  • Added Interlocked disposal guard to prevent double-dispose

IntegrationTestBase.cs (new)

  • abstract class IntegrationTestBase : WebApplicationTest<WebApplicationFactory, Program>
  • Replaces [ClassDataSource] + constructor injection across all test classes
  • Helpers: InServiceScope<T>, InServiceScopeAsync<T>, CreateRedirectFollowingClient()

McpTestHelper.cs

  • Accepts TracedWebApplicationFactory<Program> instead of WebApplicationFactory<Program>
  • CreateClient uses factory.CreateClient() — restores ActivityPropagationHandler trace correlation for all MCP tests

McpRateLimitingTests.cs

  • 6 separate [ClassDataSource] + [NotInParallel] classes → 1 class with 6 methods
  • Per-test factory isolation (fresh IHost = fresh rate limiter) removes the need for [NotInParallel]

All other test classes

  • McpTests, McpToolContractTests, McpApiTokenServiceTests, ContentRateLimitingTests, FunctionalTests, ListingSourceCodeControllerTests, RouteConfigurationServiceTests, SitemapXmlHelpersTests
  • Removed [ClassDataSource<WebApplicationFactory>], [NotInParallel], constructor injection
  • Inherit IntegrationTestBase

Key decisions

Decision Rationale
Factory.CreateClient() default AllowAutoRedirect=false + full trace correlation via ActivityPropagationHandler
CreateRedirectFollowingClient() TracedWebApplicationFactory has no CreateClient(options) overload; Inner is the only redirect path. Named + documented as explicit trade-off
Removed [NotInParallel] Per-test IHost = isolated rate limiter state per test

Reviewer notes

  • ASP0000 (BuildServiceProvider in ConfigureWebHost) is pre-existing, not introduced here
  • TracedWebApplicationFactory<T> (the Factory property) is TUnit's per-test wrapper; GlobalFactory would expose custom factory properties — not needed here

- Add TUnit.AspNetCore 1.40.5 package reference
- Create IntegrationTestBase : WebApplicationTest<WebApplicationFactory, Program>
  for per-test factory isolation, InServiceScope helpers, and trace correlation
- Rewrite WebApplicationFactory to extend TestWebApplicationFactory<Program>
  (removes IAsyncInitializer, moves to ConcurrentBag for multi-connection tracking)
- Migrate all test classes to inherit from IntegrationTestBase:
  - Remove [NotInParallel] and [ClassDataSource<WebApplicationFactory>] attributes
  - Collapse 6 McpRateLimitingTests classes into 1 (fresh DI per test = fresh rate limiter)
  - Update McpTestHelper to accept TracedWebApplicationFactory<Program>
  - Use Factory.Inner.CreateClient(options) where AllowAutoRedirect=false is needed
- Remove TUnit.Core.Interfaces import (IAsyncInitializer no longer used)
… following

TracedWebApplicationFactory.CreateClient() uses CreateDefaultClient() which
sets AllowAutoRedirect=false. Tests expecting 200/404 after app-level redirects
(e.g. / -> first chapter) need Factory.Inner.CreateClient() which defaults to
AllowAutoRedirect=true.
…pe helpers

- McpTestHelper.CreateClient: factory.Inner.CreateClient(options) -> factory.CreateClient()
  TracedWebApplicationFactory.CreateClient() already uses AllowAutoRedirect=false;
  going through Inner was silently bypassing activity propagation for all MCP tests.

- ContentRateLimitingTests: same fix - Factory.Inner -> Factory.CreateClient()

- FunctionalTests: introduce CreateRedirectFollowingClient() helper on IntegrationTestBase
  to document the explicit AllowAutoRedirect=true trade-off; all three test methods use it
  since the app issues redirects even for 404 responses.

- IntegrationTestBase: add InServiceScopeAsync<T> and InServiceScopeAsync overloads
  so callers no longer need to manually manage IServiceScope for async DB/service work.

- WebApplicationFactory: add Interlocked disposal guard to prevent double-dispose
  of SqliteConnection instances when both DisposeAsync and Dispose(bool) are called.
Copilot AI review requested due to automatic review settings May 13, 2026 16:36
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates the integration test project from a hand-rolled WebApplicationFactory<Program> + IAsyncInitializer setup to TUnit.AspNetCore's first-class WebApplicationTest<TFactory, TEntryPoint> pattern, giving per-test factory isolation, trace correlation, and removing the need for [NotInParallel] and [ClassDataSource] attributes.

Changes:

  • Adds TUnit.AspNetCore 1.40.5; subclasses TestWebApplicationFactory<Program> and tracks SQLite connections in a ConcurrentBag with Interlocked disposal guard.
  • Introduces IntegrationTestBase providing InServiceScope[Async] helpers and a CreateRedirectFollowingClient() escape hatch for redirect-following clients.
  • Refactors all integration test classes to inherit IntegrationTestBase; merges six MCP rate-limiting classes into one (each method now gets a fresh factory/limiter).

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
Directory.Packages.props Add TUnit.AspNetCore 1.40.5 package version.
EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj Reference new TUnit.AspNetCore package.
EssentialCSharp.Web.Tests/WebApplicationFactory.cs Switch base type, track connections in ConcurrentBag, add disposal guard.
EssentialCSharp.Web.Tests/IntegrationTestBase.cs New base class with service-scope helpers and redirect-following client helper.
EssentialCSharp.Web.Tests/McpTestHelper.cs Accept TracedWebApplicationFactory<Program>; drop AllowAutoRedirect=false.
EssentialCSharp.Web.Tests/McpRateLimitingTests.cs Collapse six classes into one with six tests; remove [NotInParallel].
EssentialCSharp.Web.Tests/McpTests.cs Use new base; replace factory with Factory.
EssentialCSharp.Web.Tests/McpToolContractTests.cs Use new base; replace factory with Factory.
EssentialCSharp.Web.Tests/McpApiTokenServiceTests.cs Use new base; replace factory with Factory.
EssentialCSharp.Web.Tests/ContentRateLimitingTests.cs Use new base; inline AllowAutoRedirect=false no longer needed (default).
EssentialCSharp.Web.Tests/FunctionalTests.cs Use new base and CreateRedirectFollowingClient() to preserve redirect-following behavior.
EssentialCSharp.Web.Tests/ListingSourceCodeControllerTests.cs Use new base; replace factory with Factory.
EssentialCSharp.Web.Tests/RouteConfigurationServiceTests.cs Use new base; call InServiceScope on the base.
EssentialCSharp.Web.Tests/SitemapXmlHelpersTests.cs Use new base; replace _Factory with Factory.

Comment thread EssentialCSharp.Web.Tests/WebApplicationFactory.cs
Comment thread EssentialCSharp.Web.Tests/WebApplicationFactory.cs
The shared _disposed flag was blocking base.Dispose(bool) when DisposeAsync
ran first — the base's async path may call Dispose(true), which our guard
intercepted, skipping base resource cleanup.

Fix: use a _connectionsDisposed flag that only gates the SQLite connection
cleanup (which we own). Base disposal calls are always forwarded. Also
removes the redundant GC.SuppressFinalize — the base class handles it.

Addresses Copilot PR review comments.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants