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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user