0
0

Add happy-path tests

This commit is contained in:
Rhys Ickeringill
2026-03-17 18:55:29 +11:00
parent 18c389d3c9
commit d2054dda29
12 changed files with 693 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
// Here you could define global logic that would affect all tests
// You can use attributes at the assembly level to apply to all tests in the assembly
[assembly: System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[assembly: NotInParallel]

View File

@@ -0,0 +1,73 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RAIC.Extensions.Configuration.EntityFrameworkCore.Extensions;
using RAIC.Extensions.Configuration.EntityFrameworkCore.PostgreSQL.Extensions;
using TUnit.Core.Interfaces;
namespace RAIC.Extensions.Configuration.EntityFrameworkCore.PostgreSQL.Tests;
[Table("settings")]
public record Setting : ISetting
{
[Key]
[Column("key")]
public required string Key { get; set; }
[Required]
[Column("value")]
public required string Value { get; set; }
}
public class FixtureDbContext(DbContextOptions<FixtureDbContext> options) : DbContext(options), ISettingsDbContext<DbSet<Setting>>, ISettingsDbContextFactory<FixtureDbContext>
{
public DbSet<Setting> Settings { get; set; }
public static FixtureDbContext Create(DbContextOptions<FixtureDbContext> options) => new(options);
}
public record FixtureOptions
{
public required string SomeOption { get; set; }
}
public class HostFixture : IAsyncInitializer, IServiceScopeFactory
{
private IHost? _host;
public IServiceScope CreateScope() => _host!.Services.CreateScope();
public async Task InitializeAsync()
{
var builder = Host.CreateApplicationBuilder();
// build a initial configuration
builder.Configuration
.AddInMemoryCollection(new Dictionary<string, string?>() { { nameof(FixtureOptions.SomeOption), "Memory value" }, { "ChannelName", "setting_channel" } })
.AddUserSecrets<SingleSettingTests>() // for connection string
.AddEnvironmentVariables(prefix: "ASPNETCORE_");
// add DbContext based configuration using connection string sourced from initial config
builder.Configuration.AddDbContext<FixtureDbContext>(dbContextOptions => dbContextOptions.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
builder.Services
.AddDbContext<FixtureDbContext>(opt => opt.UseNpgsql(builder.Configuration.GetConnectionString("Default")))
.AddPostgreSQLNotificationConfigurationReloadService<FixtureDbContext>(opts => opts.Bind(builder.Configuration))
.AddOptions<FixtureOptions>().Bind(builder.Configuration);
builder.Logging.AddDebug();
_host = builder.Build();
await _host.StartAsync();
_ = _host.WaitForShutdownAsync();
}
}

View File

@@ -0,0 +1,120 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace RAIC.Extensions.Configuration.EntityFrameworkCore.PostgreSQL.Tests;
/// <summary>
/// Tests where multiple settings get changed in a single transaction
/// </summary>
[ClassDataSource<HostFixture>(Shared = SharedType.PerClass)]
public class MultipleSettingTests
{
private readonly IServiceScopeFactory _hostScopeFactory;
public MultipleSettingTests(HostFixture hostFixture)
{
_hostScopeFactory = hostFixture;
}
[Test]
public async Task AddSettingsTest()
{
// Pre-condition sanity check!
using (var preconditionScope = _hostScopeFactory.CreateScope())
{
var initialOptionsState = preconditionScope.ServiceProvider.GetRequiredService<IOptionsSnapshot<FixtureOptions>>().Value;
await Assert.That(initialOptionsState.SomeOption).IsEqualTo("Memory value");
}
// Act
using (var actScope = _hostScopeFactory.CreateScope())
{
var context = actScope.ServiceProvider.GetRequiredService<FixtureDbContext>();
context.Settings.AddRange(
new() { Key = "BeforeOption", Value = "Foo" },
new() { Key = nameof(FixtureOptions.SomeOption), Value = "Db value" },
new() { Key = "AfterOption", Value = "Bar" }
);
await context.SaveChangesAsync();
}
await Task.Delay(TimeSpan.FromSeconds(2)); // wait for longer than debounce interval before checking again
// Assert
using (var assertScope = _hostScopeFactory.CreateScope())
{
var finalOptionsState = assertScope.ServiceProvider.GetRequiredService<IOptionsSnapshot<FixtureOptions>>().Value;
await Assert.That(finalOptionsState.SomeOption).IsEqualTo("Db value");
}
}
[Test]
[DependsOn(nameof(AddSettingsTest))]
public async Task UpdateSettingsTest()
{
// Pre-condition sanity check!
using (var preconditionScope = _hostScopeFactory.CreateScope())
{
var initialOptionsState = preconditionScope.ServiceProvider.GetRequiredService<IOptionsSnapshot<FixtureOptions>>().Value;
await Assert.That(initialOptionsState.SomeOption).IsEqualTo("Db value");
}
// Act
using (var actScope = _hostScopeFactory.CreateScope())
{
var context = actScope.ServiceProvider.GetRequiredService<FixtureDbContext>();
context.Settings.UpdateRange(
new() { Key = "BeforeOption", Value = "Foobar" },
new() { Key = nameof(FixtureOptions.SomeOption), Value = "Db value updated" },
new() { Key = "AfterOption", Value = "Baz" }
);
await context.SaveChangesAsync();
}
await Task.Delay(TimeSpan.FromSeconds(2)); // wait for longer than debounce interval before checking again
// Assert
using (var assertScope = _hostScopeFactory.CreateScope())
{
var finalOptionsState = assertScope.ServiceProvider.GetRequiredService<IOptionsSnapshot<FixtureOptions>>().Value;
await Assert.That(finalOptionsState.SomeOption).IsEqualTo("Db value updated");
}
}
[Test]
[DependsOn(nameof(UpdateSettingsTest))]
public async Task RemoveSettingsTest()
{
// Pre-condition sanity check!
using (var preconditionScope = _hostScopeFactory.CreateScope())
{
var initialOptionsState = preconditionScope.ServiceProvider.GetRequiredService<IOptionsSnapshot<FixtureOptions>>().Value;
await Assert.That(initialOptionsState.SomeOption).IsEqualTo("Db value updated");
}
// Act
using (var actScope = _hostScopeFactory.CreateScope())
{
var context = actScope.ServiceProvider.GetRequiredService<FixtureDbContext>();
await context.Settings.Where(s => s.Key.EndsWith("Option")).ExecuteDeleteAsync();
await context.SaveChangesAsync();
}
await Task.Delay(TimeSpan.FromSeconds(2)); // wait for longer than debounce interval before checking again
// Assert
using (var assertScope = _hostScopeFactory.CreateScope())
{
var finalOptionsState = assertScope.ServiceProvider.GetRequiredService<IOptionsSnapshot<FixtureOptions>>().Value;
await Assert.That(finalOptionsState.SomeOption).IsEqualTo("Memory value");
}
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<UserSecretsId>838c8b7c-8871-4029-8993-b595cd7aa32c</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.*" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="TUnit" Version="1.2.*" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.*" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RAIC.Extensions.Configuration.EntityFrameworkCore.PostgreSQL\RAIC.Extensions.Configuration.EntityFrameworkCore.PostgreSQL.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,19 @@
# RAIC.Extensions.Configuration.EntityFrameworkCore.PostgreSQL.Tests
Some basic happy path only tests for functionality exposed by `RAIC.Extensions.Configuration.EntityFrameworkCore.PostgreSQL.PostgreSQLNotificationConfigurationReloader`
## Requirements
Developers are expected to add their own User Secrets to the test project, containing a default connection string setting
(ie. with key `ConnectionStrings:Default`) that points to a pre-configured database that contains a `settings` table with columns
`key` & `value`.
Additionally, the required insert, delete and update triggers (**both** of them!) must have been
added and enabled on the `settings` table otherwise tests will fail
(see the [RAIC.Extensions.Configuration.EntityFrameworkCore.PostgreSQL README](../RAIC.Extensions.Configuration.EntityFrameworkCore.PostgreSQL/README.md)
for suggestions of how to do this).
## TODO
* Get EF to drop and create the database itself (using `EnsureDeleted()` & `EnsureCreated()`) instead of
forcing this on the user
* Additional tests off the happy path

View File

@@ -0,0 +1,112 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace RAIC.Extensions.Configuration.EntityFrameworkCore.PostgreSQL.Tests;
/// <summary>
/// Happy path tests
/// </summary>
[ClassDataSource<HostFixture>(Shared = SharedType.PerClass)]
public class SingleSettingTests
{
private readonly IServiceScopeFactory _hostScopeFactory;
public SingleSettingTests(HostFixture hostFixture)
{
_hostScopeFactory = hostFixture;
}
[Test]
public async Task AddSettingTest()
{
// Pre-condition sanity check!
using (var preconditionScope = _hostScopeFactory.CreateScope())
{
var initialOptionsState = preconditionScope.ServiceProvider.GetRequiredService<IOptionsSnapshot<FixtureOptions>>().Value;
await Assert.That(initialOptionsState.SomeOption).IsEqualTo("Memory value");
}
// Act
using (var actScope = _hostScopeFactory.CreateScope())
{
var context = actScope.ServiceProvider.GetRequiredService<FixtureDbContext>();
context.Settings.Add(new() { Key = nameof(FixtureOptions.SomeOption), Value = "Db value" });
await context.SaveChangesAsync();
}
await Task.Delay(TimeSpan.FromSeconds(2)); // wait for longer than debounce interval before checking again
// Assert
using (var assertScope = _hostScopeFactory.CreateScope())
{
var finalOptionsState = assertScope.ServiceProvider.GetRequiredService<IOptionsSnapshot<FixtureOptions>>().Value;
await Assert.That(finalOptionsState.SomeOption).IsEqualTo("Db value");
}
}
[Test]
[DependsOn(nameof(AddSettingTest))]
public async Task UpdateSettingTest()
{
// Pre-condition sanity check!
using (var preconditionScope = _hostScopeFactory.CreateScope())
{
var initialOptionsState = preconditionScope.ServiceProvider.GetRequiredService<IOptionsSnapshot<FixtureOptions>>().Value;
await Assert.That(initialOptionsState.SomeOption).IsEqualTo("Db value");
}
// Act
using (var actScope = _hostScopeFactory.CreateScope())
{
var context = actScope.ServiceProvider.GetRequiredService<FixtureDbContext>();
context.Settings.Update(new() { Key = nameof(FixtureOptions.SomeOption), Value = "Db value updated" });
await context.SaveChangesAsync();
}
await Task.Delay(TimeSpan.FromSeconds(2)); // wait for longer than debounce interval before checking again
// Assert
using (var assertScope = _hostScopeFactory.CreateScope())
{
var finalOptionsState = assertScope.ServiceProvider.GetRequiredService<IOptionsSnapshot<FixtureOptions>>().Value;
await Assert.That(finalOptionsState.SomeOption).IsEqualTo("Db value updated");
}
}
[Test]
[DependsOn(nameof(UpdateSettingTest))]
public async Task RemoveSettingTest()
{
// Pre-condition sanity check!
using (var preconditionScope = _hostScopeFactory.CreateScope())
{
var initialOptionsState = preconditionScope.ServiceProvider.GetRequiredService<IOptionsSnapshot<FixtureOptions>>().Value;
await Assert.That(initialOptionsState.SomeOption).IsEqualTo("Db value updated");
}
// Act
using (var actScope = _hostScopeFactory.CreateScope())
{
var context = actScope.ServiceProvider.GetRequiredService<FixtureDbContext>();
await context.Settings.Where(s => s.Key == nameof(FixtureOptions.SomeOption)).ExecuteDeleteAsync();
await context.SaveChangesAsync();
}
await Task.Delay(TimeSpan.FromSeconds(2)); // wait for longer than debounce interval before checking again
// Assert
using (var assertScope = _hostScopeFactory.CreateScope())
{
var finalOptionsState = assertScope.ServiceProvider.GetRequiredService<IOptionsSnapshot<FixtureOptions>>().Value;
await Assert.That(finalOptionsState.SomeOption).IsEqualTo("Memory value");
}
}
}