From 375d5506b2d5f105ecad0086a8badc20457d5c53 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 14:49:40 +0000 Subject: [PATCH] Consolidate reusable test utilities in SimpleModule.Tests.Shared Move InMemoryStorageProvider out of SimpleModule.FileStorage.Tests and rename it to InMemoryStorage under SimpleModule.Tests.Shared so any test project that needs an in-memory IStorageProvider can reuse it without copying the class. Update FileStorageServiceTests to use the shared type. Add FakeJobExecutionContext in SimpleModule.Tests.Shared. It implements IJobExecutionContext (public contract) and captures progress reports and log messages in-memory so tests can assert on job behavior without constructing a ProgressChannel or the internal DefaultJobExecutionContext. Add TestDatasetsDbContext in SimpleModule.Tests.Shared. It is a sealed subclass of DatasetsDbContext with a static Create() factory that owns a private in-memory SQLite connection and disposes it with the context. No change to the BackgroundJobs InternalsVisibleTo attribute: the new utilities only depend on public contract types. --- .../FileStorageServiceTests.cs | 5 +- .../BackgroundJobs/FakeJobExecutionContext.cs | 54 ++++++++++++++++++ .../Datasets/TestDatasetsDbContext.cs | 57 +++++++++++++++++++ .../Storage/InMemoryStorage.cs | 8 ++- 4 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 tests/SimpleModule.Tests.Shared/BackgroundJobs/FakeJobExecutionContext.cs create mode 100644 tests/SimpleModule.Tests.Shared/Datasets/TestDatasetsDbContext.cs rename modules/FileStorage/tests/SimpleModule.FileStorage.Tests/InMemoryStorageProvider.cs => tests/SimpleModule.Tests.Shared/Storage/InMemoryStorage.cs (90%) diff --git a/modules/FileStorage/tests/SimpleModule.FileStorage.Tests/FileStorageServiceTests.cs b/modules/FileStorage/tests/SimpleModule.FileStorage.Tests/FileStorageServiceTests.cs index 51988864..b827bf4f 100644 --- a/modules/FileStorage/tests/SimpleModule.FileStorage.Tests/FileStorageServiceTests.cs +++ b/modules/FileStorage/tests/SimpleModule.FileStorage.Tests/FileStorageServiceTests.cs @@ -4,13 +4,14 @@ using Microsoft.Extensions.Options; using SimpleModule.Database; using SimpleModule.FileStorage.Contracts; +using SimpleModule.Tests.Shared.Storage; namespace SimpleModule.FileStorage.Tests; public sealed class FileStorageServiceTests : IDisposable { private readonly FileStorageDbContext _db; - private readonly InMemoryStorageProvider _storageProvider; + private readonly InMemoryStorage _storageProvider; private readonly FileStorageService _service; public FileStorageServiceTests() @@ -32,7 +33,7 @@ public FileStorageServiceTests() _db.Database.OpenConnection(); _db.Database.EnsureCreated(); - _storageProvider = new InMemoryStorageProvider(); + _storageProvider = new InMemoryStorage(); _service = new FileStorageService( _db, _storageProvider, diff --git a/tests/SimpleModule.Tests.Shared/BackgroundJobs/FakeJobExecutionContext.cs b/tests/SimpleModule.Tests.Shared/BackgroundJobs/FakeJobExecutionContext.cs new file mode 100644 index 00000000..b63c92dd --- /dev/null +++ b/tests/SimpleModule.Tests.Shared/BackgroundJobs/FakeJobExecutionContext.cs @@ -0,0 +1,54 @@ +using System.Text.Json; +using SimpleModule.BackgroundJobs.Contracts; + +namespace SimpleModule.Tests.Shared.BackgroundJobs; + +/// +/// Test fake for . Captures progress reports +/// and log messages in-memory so tests can assert on what a job did without +/// wiring up a ProgressChannel or the real DefaultJobExecutionContext. +/// +public sealed class FakeJobExecutionContext : IJobExecutionContext +{ + private readonly string? _serializedData; + private readonly List _progressReports = []; + private readonly List _logMessages = []; + + public FakeJobExecutionContext(JobId jobId, string? serializedData = null) + { + JobId = jobId; + _serializedData = serializedData; + } + + public FakeJobExecutionContext(JobId jobId, object data) + : this(jobId, JsonSerializer.Serialize(data)) { } + + public FakeJobExecutionContext() + : this(JobId.From(Guid.NewGuid()), serializedData: null) { } + + public JobId JobId { get; } + + public IReadOnlyList ProgressReports => _progressReports; + + public IReadOnlyList LogMessages => _logMessages; + + public T GetData() + { + if (string.IsNullOrEmpty(_serializedData)) + { + throw new InvalidOperationException("No data was provided for this job."); + } + + return JsonSerializer.Deserialize(_serializedData) + ?? throw new InvalidOperationException( + $"Failed to deserialize job data as {typeof(T).Name}." + ); + } + + public void ReportProgress(int percentage, string? message = null) => + _progressReports.Add(new ProgressReport(percentage, message)); + + public void Log(string message) => _logMessages.Add(message); + + public readonly record struct ProgressReport(int Percentage, string? Message); +} diff --git a/tests/SimpleModule.Tests.Shared/Datasets/TestDatasetsDbContext.cs b/tests/SimpleModule.Tests.Shared/Datasets/TestDatasetsDbContext.cs new file mode 100644 index 00000000..7c0117e9 --- /dev/null +++ b/tests/SimpleModule.Tests.Shared/Datasets/TestDatasetsDbContext.cs @@ -0,0 +1,57 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using SimpleModule.Database; +using SimpleModule.Datasets; + +namespace SimpleModule.Tests.Shared.Datasets; + +/// +/// Test subclass of backed by a private +/// in-memory SQLite connection. Use the static factory +/// to obtain a ready-to-use, self-owning context; disposing the context +/// closes the underlying connection. +/// +public sealed class TestDatasetsDbContext : DatasetsDbContext +{ + private readonly SqliteConnection _connection; + + private TestDatasetsDbContext( + DbContextOptions options, + IOptions dbOptions, + SqliteConnection connection + ) + : base(options, dbOptions) + { + _connection = connection; + } + + /// + /// Creates a with a fresh in-memory + /// SQLite database. Dispose the returned context to release the + /// connection. + /// + public static TestDatasetsDbContext Create() + { + var connection = new SqliteConnection("Data Source=:memory:"); + connection.Open(); + + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + + var dbOptions = Options.Create( + new DatabaseOptions { DefaultConnection = "Data Source=:memory:", Provider = "Sqlite" } + ); + + var context = new TestDatasetsDbContext(options, dbOptions, connection); + context.Database.EnsureCreated(); + return context; + } + + public override void Dispose() + { + base.Dispose(); + _connection.Dispose(); + } +} diff --git a/modules/FileStorage/tests/SimpleModule.FileStorage.Tests/InMemoryStorageProvider.cs b/tests/SimpleModule.Tests.Shared/Storage/InMemoryStorage.cs similarity index 90% rename from modules/FileStorage/tests/SimpleModule.FileStorage.Tests/InMemoryStorageProvider.cs rename to tests/SimpleModule.Tests.Shared/Storage/InMemoryStorage.cs index ed3ba23a..867301f0 100644 --- a/modules/FileStorage/tests/SimpleModule.FileStorage.Tests/InMemoryStorageProvider.cs +++ b/tests/SimpleModule.Tests.Shared/Storage/InMemoryStorage.cs @@ -1,9 +1,13 @@ using System.Collections.Concurrent; using SimpleModule.Storage; -namespace SimpleModule.FileStorage.Tests; +namespace SimpleModule.Tests.Shared.Storage; -public sealed class InMemoryStorageProvider : IStorageProvider +/// +/// In-memory for tests. Stores files in a +/// concurrent dictionary keyed by normalized path. +/// +public sealed class InMemoryStorage : IStorageProvider { private readonly ConcurrentDictionary< string,