Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integration test config rework #746

Merged
merged 6 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ but this project DOES NOT adhere to [Semantic Versioning](http://semver.org/).
* Remove StyleCop completely
* JSON serializer for CQRS now shares options with ASP.NET Core's `JsonOptions` by default
* Integrates with Microsoft.AspNetCore.OpenApi better
* Connection string injection is extracted from `ConfigurationOverrides` into `TestConnectionString` source
* `DbContextInitializer` works with Npgsql now

## 8.1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,66 +3,29 @@

namespace LeanCode.IntegrationTestHelpers;

public class ConfigurationOverrides : IConfigurationSource
public class ConfigurationOverrides(Dictionary<string, string?> customValues) : IConfigurationSource
{
public const LogEventLevel MinimumLevelDefault = LogEventLevel.Warning;
public const bool EnableInternalLogsDefault = false;

private readonly LogEventLevel minimumLevel;
private readonly bool enableInternalLogs;
private readonly string connectionStringBase;
private readonly string connectionStringKey;

public ConfigurationOverrides(
string connectionStringBase,
string connectionStringKey,
LogEventLevel? minimumLevel = null,
bool? enableInternalLogs = null
public static ConfigurationOverrides LoggingOverrides(
LogEventLevel minimumLevel = LogEventLevel.Information,
bool enableInternalLogs = true
)
{
this.connectionStringBase = connectionStringBase;
this.connectionStringKey = connectionStringKey;
this.minimumLevel = minimumLevel ?? MinimumLevelDefault;
this.enableInternalLogs = enableInternalLogs ?? EnableInternalLogsDefault;
return new ConfigurationOverrides(
new Dictionary<string, string?>
{
[Logging.IHostBuilderExtensions.MinimumLogLevelKey] = minimumLevel.ToString(),
[Logging.IHostBuilderExtensions.EnableDetailedInternalLogsKey] = enableInternalLogs.ToString(),
}
);
}

public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new Provider(minimumLevel, enableInternalLogs, connectionStringBase, connectionStringKey);
}
public IConfigurationProvider Build(IConfigurationBuilder builder) => new Provider(customValues);

private sealed class Provider : ConfigurationProvider
{
private readonly LogEventLevel minimumLevel;
private readonly bool enableInternalLogs;
private readonly string connectionStringBase;
private readonly string connectionStringKey;

public Provider(
LogEventLevel minimumLevel,
bool enableInternalLogs,
string connectionStringBase,
string connectionStringKey
)
public Provider(Dictionary<string, string?> data)
{
this.minimumLevel = minimumLevel;
this.enableInternalLogs = enableInternalLogs;
this.connectionStringBase = connectionStringBase;
this.connectionStringKey = connectionStringKey;
}

public override void Load()
{
var dbName = $"integration_tests_{Guid.NewGuid():N}";
var rest = Environment.GetEnvironmentVariable(connectionStringBase);
var dbConnStr = $"Database={dbName};" + rest;

Data = new Dictionary<string, string?>
{
[connectionStringKey] = dbConnStr,
[LeanCode.Logging.IHostBuilderExtensions.EnableDetailedInternalLogsKey] = enableInternalLogs.ToString(),
[LeanCode.Logging.IHostBuilderExtensions.MinimumLogLevelKey] = minimumLevel.ToString(),
};
Data = data;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Npgsql;
using Polly;
using Polly.Retry;

Expand All @@ -12,14 +13,15 @@ public class DbContextInitializer<T>(IServiceProvider serviceProvider) : IHosted
{
private static readonly AsyncRetryPolicy CreatePolicy = Policy
.Handle<SqlException>(e => e.Number == 5177)
.Or<NpgsqlException>(e => e.IsTransient)
.WaitAndRetryAsync([TimeSpan.FromSeconds(0.5), TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3)]);

private readonly Serilog.ILogger logger = Serilog.Log.ForContext<DbContextInitializer<T>>();

public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = serviceProvider.CreateAsyncScope();
using var context = scope.ServiceProvider.GetRequiredService<T>();
await using var scope = serviceProvider.CreateAsyncScope();
await using var context = scope.ServiceProvider.GetRequiredService<T>();
logger.Information("Creating database for context {ContextType}", context.GetType());
// HACK: should mitigate (slightly) the bug in MSSQL that prevents us from creating
// new databases.
Expand All @@ -29,16 +31,30 @@ await CreatePolicy.ExecuteAsync(
{
await context.Database.EnsureDeletedAsync(token);
await context.Database.EnsureCreatedAsync(token);

if (context.Database.GetDbConnection() is NpgsqlConnection connection)
{
if (connection.State == System.Data.ConnectionState.Closed)
{
await connection.OpenAsync(token);
await connection.ReloadTypesAsync();
await connection.CloseAsync();
}
else
{
await connection.ReloadTypesAsync();
}
}
},
cancellationToken
);
}

public async Task StopAsync(CancellationToken cancellationToken)
{
using var scope = serviceProvider.CreateAsyncScope();
using var context = scope.ServiceProvider.GetRequiredService<T>();
await using var scope = serviceProvider.CreateAsyncScope();
await using var context = scope.ServiceProvider.GetRequiredService<T>();
logger.Information("Dropping database for context {ContextType}", context.GetType());
await context.Database.EnsureDeletedAsync(cancellationToken);
await context.Database.EnsureDeletedAsync(CancellationToken.None);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<PackageReference Include="IdentityModel" />

<PackageReference Include="Polly" />
<PackageReference Include="Npgsql" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ namespace LeanCode.IntegrationTestHelpers;
public abstract class LeanCodeTestFactory<TStartup> : WebApplicationFactory<TStartup>
where TStartup : class
{
protected abstract ConfigurationOverrides Configuration { get; }
protected abstract TestConnectionString ConnectionStringConfig { get; }

protected virtual ConfigurationOverrides ConfigurationOverrides { get; } =
ConfigurationOverrides.LoggingOverrides();

public virtual JsonSerializerOptions JsonOptions { get; }

Expand Down Expand Up @@ -81,7 +84,8 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
builder
.ConfigureAppConfiguration(config =>
{
config.Add(Configuration);
config.Add(ConfigurationOverrides);
config.Add(ConnectionStringConfig);
})
.ConfigureServices(services =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Microsoft.Extensions.Configuration;
using Serilog.Events;

namespace LeanCode.IntegrationTestHelpers;

public class TestConnectionString(string connectionStringBaseKey, string connectionStringKey) : IConfigurationSource
{
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
var newBuilder = RebuildWithoutSelf(builder);
return new Provider(connectionStringBaseKey, connectionStringKey, newBuilder);
}

private IConfigurationRoot RebuildWithoutSelf(IConfigurationBuilder builder)
{
var newBuilder = new ConfigurationBuilder();
foreach (var src in builder.Sources)
{
if (src != this)
{
newBuilder.Add(src);
}
}

return newBuilder.Build();
}

private sealed class Provider(
string connectionStringBaseKey,
string connectionStringKey,
IConfiguration parentConfig
) : ConfigurationProvider
{
public override void Load()
{
var baseConnStr =
parentConfig[connectionStringBaseKey]
?? throw new KeyNotFoundException(
$"Cannot find base connection string under key {connectionStringBaseKey}"
);

var dbName = $"integration_tests_{Guid.NewGuid():N}";
var dbConnStr = $"Database={dbName};" + baseConnStr;

Data = new Dictionary<string, string?> { [connectionStringKey] = dbConnStr };
}
}
}
2 changes: 1 addition & 1 deletion test/LeanCode.IntegrationTests/TestApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ static TestApp()
}
}

protected override ConfigurationOverrides Configuration => TestDatabaseConfig.Create().GetConfigurationOverrides();
protected override TestConnectionString ConnectionStringConfig => TestDatabaseConfig.Create().GetConnectionString();

protected override IEnumerable<Assembly> GetTestAssemblies()
{
Expand Down
10 changes: 5 additions & 5 deletions test/LeanCode.IntegrationTests/TestDatabaseConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public abstract class TestDatabaseConfig
{
public const string ConfigEnvName = "LeanCodeIntegrationTests__Database";

public abstract ConfigurationOverrides GetConfigurationOverrides();
public abstract TestConnectionString GetConnectionString();
public abstract void ConfigureDbContext(DbContextOptionsBuilder builder, IConfiguration config);
public abstract void ConfigureMassTransitOutbox(IEntityFrameworkOutboxConfigurator configurator);

Expand All @@ -31,8 +31,8 @@ public static TestDatabaseConfig Create()

public class SqlServerTestDatabaseConfig : TestDatabaseConfig
{
public override ConfigurationOverrides GetConfigurationOverrides() =>
new("SqlServer__ConnectionStringBase", "SqlServer:ConnectionString");
public override TestConnectionString GetConnectionString() =>
new("SqlServer:ConnectionStringBase", "SqlServer:ConnectionString");

public override void ConfigureDbContext(DbContextOptionsBuilder builder, IConfiguration config)
{
Expand All @@ -47,8 +47,8 @@ public override void ConfigureMassTransitOutbox(IEntityFrameworkOutboxConfigurat

public class PostgresTestConfig : TestDatabaseConfig
{
public override ConfigurationOverrides GetConfigurationOverrides() =>
new("Postgres__ConnectionStringBase", "Postgres:ConnectionString");
public override TestConnectionString GetConnectionString() =>
new("Postgres:ConnectionStringBase", "Postgres:ConnectionString");

public override void ConfigureDbContext(DbContextOptionsBuilder builder, IConfiguration config)
{
Expand Down

This file was deleted.

18 changes: 15 additions & 3 deletions test/Testing/LeanCode.IntegrationTestHelpers.Tests/TestApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,24 @@
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog.Events;

namespace LeanCode.IntegrationTestHelpers.Tests;

public class TestApp : LeanCodeTestFactory<App.Startup>
{
protected override ConfigurationOverrides Configuration { get; } =
new("SqlServer__ConnectionStringBase", "SqlServer:ConnectionString", Serilog.Events.LogEventLevel.Error, false);
protected override TestConnectionString ConnectionStringConfig { get; } =
new("SqlServer:ConnectionStringBase", "SqlServer:ConnectionString");

protected override ConfigurationOverrides ConfigurationOverrides { get; } =
new(
new()
{
[IHostBuilderExtensions.MinimumLogLevelKey] = "Error",
[IHostBuilderExtensions.EnableDetailedInternalLogsKey] = "false",
["SqlServer:ConnectionStringBase"] = "",
}
);

protected override IEnumerable<Assembly> GetTestAssemblies()
{
Expand All @@ -31,7 +42,8 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
builder
.ConfigureAppConfiguration(config =>
{
config.Add(Configuration);
config.Add(ConfigurationOverrides);
config.Add(ConnectionStringConfig);
})
.ConfigureServices(services =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Xunit;

namespace LeanCode.IntegrationTestHelpers.Tests;

public class TestConnectionStringTests
{
private const string ConnectionStringKey = "ConnectionString";
private const string BaseKey = "Base";

[Fact]
public void Generates_connection_string()
{
var config = Build("");

config[ConnectionStringKey].Should().NotBeNull();
}

[Fact]
public void Preserves_base_connection_string()
{
var config = Build("");

config[BaseKey].Should().NotBeNull();
}

[Fact]
public void Final_connection_string_contains_Database_parameter()
{
var config = Build("");

config[ConnectionStringKey].Should().Contain("Database=");
}

[Fact]
public void Final_connection_string_contains_base_connection_string()
{
const string BaseConnectionString = "Some=1;Parameter=2";
var config = Build(BaseConnectionString);

config[ConnectionStringKey].Should().Contain(BaseConnectionString);
}

private IConfiguration Build(string baseConnectionString)
{
var builder = new ConfigurationBuilder();
builder.Add(new ConfigurationOverrides(new() { [BaseKey] = baseConnectionString }));
builder.Add(new TestConnectionString(BaseKey, ConnectionStringKey));
return builder.Build();
}
}
Loading