test(org): isolate in-memory store per fixture
TestWebApplicationFactory instances shared the same in-memory database
because EF Core's UseInMemoryDatabase("InMemory") returns the same
backing store to every DbContext that asks for it under the same
connection string, in the same process. Whichever fixture started
first defined the state, and every subsequent fixture inherited it,
making tests silently order-dependent and flaky.
Fix:
- Yavsc.Tests.Shared/InMemoryDatabaseName: helper that suffixes the
in-memory connection string with a per-fixture GUID.
- TestWebApplicationFactory: instance GUID + ConnectionStrings__
YavscConnection set as an environment variable in the constructor
and cleared in Dispose, so each factory gets its own backing store.
Env var is needed because IdentityServer8.EntityFramework exposes
ConfigureDbContext as Action<DbContextOptionsBuilder> with no
service-provider access, so the connection string is captured at
registration time. AddEnvironmentVariables is the last provider in
the config pipeline and wins regardless.
- WebServerFixture: process-static GUID (WebHostFixture is a
per-process singleton by design, so the test collection shares one
store; the GUID still isolates from TestWebApplicationFactory).
- AddIdentityDBAndStores: read the connection string at DbContext
construction time via the (sp, options) overload of AddDbContext,
so test fixtures can override it via the host's IConfiguration.
IdentityServer stores cannot do the same without subclassing the
framework's DbContexts; the env var path is the documented escape
hatch in HostingExtensions.AddIdentityServer.
- UsesInMemoryProvider: StartsWith instead of equality, so
'InMemory-{guid}' is still recognised as an in-memory connection
string.
Regression sentinel in
Controllers/TestWebApplicationFactoryIsolationTests: two factories
seed a marker client in the first, the second must not see it.
Suite: 45/45 over 3 stable runs, 13-15s each.
This commit is contained in:
parent
d05ac52829
commit
61b41f0c55
5 changed files with 201 additions and 6 deletions
|
|
@ -169,10 +169,20 @@ public static class HostingExtensions
|
|||
public static IdentityBuilder AddIdentityDBAndStores(this WebApplicationBuilder builder)
|
||||
{
|
||||
IServiceCollection services = builder.Services;
|
||||
var connectionString = builder.Configuration.GetConnectionString(Constants.YavscConnectionStringName);
|
||||
|
||||
services.AddDbContext<ApplicationDbContext>(options =>
|
||||
services.AddDbContext<ApplicationDbContext>((sp, options) =>
|
||||
{
|
||||
// Read the connection string at DbContext construction time
|
||||
// rather than at AddDbContext registration time, so test
|
||||
// fixtures (e.g. WebApplicationFactory<Program>) can
|
||||
// override the value via the host's IConfiguration before
|
||||
// any DbContext is built. Reading it eagerly at the top of
|
||||
// this method would freeze whatever was in configuration
|
||||
// when Program.Main ran — too early for the test host's
|
||||
// ConfigureAppConfiguration / UseSetting hooks to apply.
|
||||
var connectionString = sp.GetRequiredService<IConfiguration>()
|
||||
.GetConnectionString(Constants.YavscConnectionStringName);
|
||||
|
||||
if (UsesInMemoryProvider(connectionString))
|
||||
{
|
||||
options.UseInMemoryDatabase(connectionString);
|
||||
|
|
@ -317,9 +327,20 @@ public static class HostingExtensions
|
|||
options.ClaimsIdentity.RoleClaimType = Constants.RoleClaimType;
|
||||
});
|
||||
var migrationsAssembly = typeof(Program).GetTypeInfo().Assembly.GetName().Name;
|
||||
var connectionString = builder.Configuration.GetConnectionString(Constants.YavscConnectionStringName);
|
||||
|
||||
string sqliteConnectionString = $"Data Source={Path.Combine(Path.GetTempPath(), "yavsc_test.db")}";
|
||||
// The IdentityServer8.EntityFramework ConfigurationStoreOptions
|
||||
// and OperationalStoreOptions expose ConfigureDbContext as an
|
||||
// Action<DbContextOptionsBuilder> with no service-provider
|
||||
// access, so the connection string has to be captured here at
|
||||
// registration time. For the production runtime this is fine:
|
||||
// the connection string does not change after startup. For
|
||||
// tests, this is the one knob we cannot push into the per-fixture
|
||||
// config pipeline; the TestWebApplicationFactory bridge instead
|
||||
// sets ConnectionStrings__YavscConnection as an environment
|
||||
// variable, which AddEnvironmentVariables picks up as the last
|
||||
// configuration provider in AddConfiguration. See
|
||||
// Yavsc.Server/Helpers/ConfigHelpers.cs.
|
||||
var connectionString = builder.Configuration.GetConnectionString(Constants.YavscConnectionStringName);
|
||||
|
||||
var identityServerBuilder = builder.Services.AddIdentityServer(options =>
|
||||
{
|
||||
|
|
@ -600,7 +621,13 @@ public static class HostingExtensions
|
|||
|
||||
private static bool UsesInMemoryProvider(string connectionString)
|
||||
{
|
||||
return string.Equals(connectionString, InMemoryProviderName, StringComparison.OrdinalIgnoreCase);
|
||||
// Test fixtures may suffix the connection string with a
|
||||
// per-fixture GUID (see InMemoryDatabaseName in
|
||||
// Yavsc.Tests.Shared) to keep their in-memory stores
|
||||
// isolated. The base name "InMemory" is still what
|
||||
// identifies an in-memory provider — anything starting
|
||||
// with it is one.
|
||||
return connectionString.StartsWith(InMemoryProviderName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static Action<DbContext, bool> EnsureDefaultApplicationScopes()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue