Created
June 3, 2019 19:42
-
-
Save scionwest/7e58253a848d74caf79f119f9eb33ddd to your computer and use it in GitHub Desktop.
Generate Databases per Integration Test
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.AspNetCore; | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.AspNetCore.Mvc.Testing; | |
using Microsoft.AspNetCore.TestHost; | |
using Microsoft.EntityFrameworkCore; | |
using Microsoft.Extensions.Configuration; | |
using MySql.Data.MySqlClient; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Runtime.CompilerServices; | |
namespace ToNote | |
{ | |
/// <summary> | |
/// Provides a Test Server that will setup a new database and optionally seed the database. | |
/// Any database created from this factory will be automatically deleted unless marked with <see cref="RetainDatabase{TContext}"/> | |
/// </summary> | |
/// <typeparam name="TStartup">The startup class that will be register services, configurations and middleware.</typeparam> | |
public class DatabaseAppFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class | |
{ | |
private readonly List<TestDatabaseSetup> databaseSetups = new List<TestDatabaseSetup>(); | |
private readonly string userSecrets; | |
public DatabaseAppFactory() { } | |
/// <summary> | |
/// User Secrets of the project being tested so that secrets don't need to be copied to the test project. | |
/// </summary> | |
/// <param name="userSecrets">The Subject Under Test` user secrets.</param> | |
public DatabaseAppFactory(string userSecrets) => this.userSecrets = userSecrets; | |
/// <summary> | |
/// Marks the database being created for the test as needing to be retained after the test is completed. | |
/// The database will not be deleted. | |
/// </summary> | |
public DatabaseAppFactory<TStartup> RetainDatabase<TContext>() where TContext : DbContext | |
{ | |
Type contextType = typeof(TContext); | |
TestDatabaseSetup databaseSetup = this.databaseSetups.First(setup => setup.DbContextType == contextType); | |
databaseSetup.RetainDatabase = true; | |
return this; | |
} | |
/// <summary> | |
/// Requests the creation of a test database using the given <see cref="DbContext"/>. | |
/// </summary> | |
/// <typeparam name="TContext">The context to create a database for.</typeparam> | |
/// <param name="connectionStringKey"> | |
/// The <see cref="IConfiguration"/> | |
/// <para>key used to find the connection string. | |
/// Assuming a connection string called <c>MyDatabase</c>, then if the fully qualified key is | |
/// <c>ConnectionStrings:MyDatabase</c> you may only provide <c>MyDatabase</c>. | |
/// The factory will discover the full connection string value from the standard convention expected. | |
/// </para> | |
/// <para> | |
/// If you do not use the standard convention, instead using a different fully qualified name such as <c>Data:MyApp:MyDatabase</c> | |
/// then you will need to specify the fully qualified configuration key for the factory to discover the value. | |
/// </para> | |
/// </param> | |
/// <param name="seeder">A callback that will provide the caller with an instance of the <see cref="DbContext"/> for seeding the test database with data.</param> | |
/// <param name="callingTest">The test being executed for this factory.</param> | |
public DatabaseAppFactory<TStartup> WithDataContext<TContext>(string connectionStringKey, Action<TContext> seeder = null, [CallerMemberName] string callingTest = null) where TContext : DbContext | |
{ | |
this.databaseSetups.Add(new TestDatabaseSetup | |
{ | |
ConnectionStringKey = connectionStringKey, | |
DbContextType = typeof(TContext), | |
Seeder = seeder, | |
ExecutingTest = callingTest, | |
}); | |
return this; | |
} | |
/// <summary> | |
/// Delete any database that is not marked as needing to be retained. | |
/// </summary> | |
/// <param name="disposing">Ignored by this factory.</param> | |
protected override void Dispose(bool disposing) | |
{ | |
foreach (TestDatabaseSetup setup in this.databaseSetups) | |
{ | |
if (setup.RetainDatabase) | |
{ | |
continue; | |
} | |
DbContext dbContext = (DbContext)this.Server.Host.Services.GetService(setup.DbContextType); | |
dbContext.Database.EnsureDeleted(); | |
} | |
base.Dispose(disposing); | |
} | |
/// <summary> | |
/// Configure the host so that it uses the Startup needed by the test along with | |
/// any user secrets if they exist. | |
/// </summary> | |
/// <returns>Returns a configured host builder.</returns> | |
protected override IWebHostBuilder CreateWebHostBuilder() | |
{ | |
IWebHostBuilder hostBuilder = WebHost.CreateDefaultBuilder() | |
.ConfigureAppConfiguration(configBuilder => | |
{ | |
if (string.IsNullOrEmpty(this.userSecrets)) return; | |
configBuilder.AddUserSecrets(this.userSecrets); | |
}) | |
.UseStartup<TStartup>(); | |
return hostBuilder; | |
} | |
/// <summary> | |
/// Sets up the content root of the host. | |
/// </summary> | |
/// <param name="builder">Pre-configured host builder.</param> | |
protected override void ConfigureWebHost(IWebHostBuilder builder) | |
{ | |
builder.UseSolutionRelativeContentRoot("."); | |
base.ConfigureWebHost(builder); | |
} | |
/// <summary> | |
/// Creates the in-memory web server and creates the test databases registered. | |
/// </summary> | |
/// <param name="builder">The configured host builder.</param> | |
/// <returns></returns> | |
protected override TestServer CreateServer(IWebHostBuilder builder) | |
{ | |
foreach (TestDatabaseSetup setup in this.databaseSetups) | |
{ | |
this.SetupDb(builder, setup); | |
} | |
TestServer server = base.CreateServer(builder); | |
foreach (TestDatabaseSetup setup in this.databaseSetups) | |
{ | |
DbContext dbContext = (DbContext)server.Host.Services.GetService(setup.DbContextType); | |
dbContext.Database.EnsureCreated(); | |
setup.Seeder?.DynamicInvoke(dbContext); | |
} | |
return server; | |
} | |
/// <summary> | |
/// Replaces the connection string for each test database registerd. | |
/// </summary> | |
/// <param name="hostBuilder">Configured host builder</param> | |
/// <param name="testSetup">Setup of a database context.</param> | |
private void SetupDb(IWebHostBuilder hostBuilder, TestDatabaseSetup testSetup) | |
{ | |
hostBuilder.ConfigureAppConfiguration((hostContext, configBuilder) => | |
{ | |
IConfiguration config = configBuilder.Build(); | |
string connectionString = string.IsNullOrEmpty(config.GetConnectionString(testSetup.ConnectionStringKey)) | |
? config[testSetup.ConnectionStringKey] | |
: config.GetConnectionString(testSetup.ConnectionStringKey); | |
string timeStamp = DateTime.Now.ToString("HHmmss.fff"); | |
var connectionStringBuilder = new MySqlConnectionStringBuilder(connectionString) | |
{ | |
Database = $"Tests-{testSetup.ExecutingTest}-{timeStamp}" | |
}; | |
string configKey = string.IsNullOrEmpty(config.GetConnectionString(testSetup.ConnectionStringKey)) | |
? testSetup.ConnectionStringKey | |
: $"ConnectionStrings:{testSetup.ConnectionStringKey}"; | |
var memoryConfig = new Dictionary<string, string> | |
{ | |
{ configKey, connectionStringBuilder.ToString() } | |
}; | |
configBuilder.AddInMemoryCollection(memoryConfig); | |
}); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.EntityFrameworkCore; | |
using System; | |
namespace ToNote | |
{ | |
internal class TestDatabaseSetup | |
{ | |
public Delegate Seeder { get; set; } | |
public Type DbContextType { get; set; } | |
public string ConnectionStringKey { get; set; } | |
public string ExecutingTest { get; set; } | |
public bool RetainDatabase { get; set; } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment