test(story-service): per-test fixture instance + unique InMemoryDb name

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 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-01 00:40:51 -04:00
parent 31f26655ba
commit 66c456c1c8

View File

@@ -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<IStoryMasterRepository> _master = null!;
@@ -829,12 +834,15 @@ internal static class StoryServiceTestHelpers
/// <summary>
/// Returns a minimal <see cref="SVSimDbContext"/> backed by the EF InMemory provider.
/// Safe for non-reward tests that never actually query the DB.
/// Each call should use a unique <paramref name="dbName"/> to prevent test bleed-through.
/// The supplied <paramref name="dbName"/> 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 <c>nameof(SetUp)</c>, which collapsed every
/// test in the fixture onto the same store and broke under parallel execution.
/// </summary>
public static SVSimDbContext NewInMemoryDb(string dbName)
{
var options = new Microsoft.EntityFrameworkCore.DbContextOptionsBuilder<SVSimDbContext>()
.UseInMemoryDatabase(dbName)
.UseInMemoryDatabase($"{dbName}-{Guid.NewGuid():N}")
.Options;
return new SVSimDbContext(
Microsoft.Extensions.Logging.Abstractions.NullLogger<SVSimDbContext>.Instance,