From 66c456c1c82533296b7b616724f5596a4cfd1b64 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 1 Jun 2026 00:40:51 -0400 Subject: [PATCH] test(story-service): per-test fixture instance + unique InMemoryDb name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NUnit's default FixtureLifeCycle is SingleInstance — every test in a class shares one fixture instance, so [SetUp]-initialised fields like _master / _viewer / _service are reset on every test against the same object. Under serial execution that's fine; under parallel execution concurrent SetUps wipe each other's Mock setups and the service code NREs trying to dereference unconfigured stubs. Compounding it, NewInMemoryDb was being called with nameof(SetUp) which is the literal string "SetUp", so every test in the fixture also shared the same EF InMemory database (the provider keys stores by name). Two fixes: - [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] on StoryServiceTests so each test gets its own instance with its own Mocks. - Suffix the InMemoryDb name with a Guid so concurrent callers never share a store. Co-Authored-By: Claude Opus 4.7 --- SVSim.UnitTests/Story/StoryServiceTests.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/SVSim.UnitTests/Story/StoryServiceTests.cs b/SVSim.UnitTests/Story/StoryServiceTests.cs index 7e1e20a..1599ad5 100644 --- a/SVSim.UnitTests/Story/StoryServiceTests.cs +++ b/SVSim.UnitTests/Story/StoryServiceTests.cs @@ -16,6 +16,11 @@ using SVSim.UnitTests.Infrastructure; namespace SVSim.UnitTests.Story; [TestFixture] +// One instance per test case so parallel tests don't race on the SetUp-initialised +// _master / _viewer / _service fields. NUnit's default SingleInstance shares the +// fixture instance across all tests in the class; under ParallelScope.All, concurrent +// SetUps wipe each other's Mock setups and we see NullReferenceExceptions in service code. +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public class StoryServiceTests { private Mock _master = null!; @@ -829,12 +834,15 @@ internal static class StoryServiceTestHelpers /// /// Returns a minimal backed by the EF InMemory provider. /// Safe for non-reward tests that never actually query the DB. - /// Each call should use a unique to prevent test bleed-through. + /// The supplied is suffixed with a fresh Guid so concurrent + /// callers never share a database — EF InMemory keys by name, and the previous callers + /// all passed the literal string "SetUp" via nameof(SetUp), which collapsed every + /// test in the fixture onto the same store and broke under parallel execution. /// public static SVSimDbContext NewInMemoryDb(string dbName) { var options = new Microsoft.EntityFrameworkCore.DbContextOptionsBuilder() - .UseInMemoryDatabase(dbName) + .UseInMemoryDatabase($"{dbName}-{Guid.NewGuid():N}") .Options; return new SVSimDbContext( Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance,