Fusonic.Extensions.UnitTests.ServiceProvider
9.5.0
Prefix Reserved
dotnet add package Fusonic.Extensions.UnitTests.ServiceProvider --version 9.5.0
NuGet\Install-Package Fusonic.Extensions.UnitTests.ServiceProvider -Version 9.5.0
<PackageReference Include="Fusonic.Extensions.UnitTests.ServiceProvider" Version="9.5.0" />
paket add Fusonic.Extensions.UnitTests.ServiceProvider --version 9.5.0
#r "nuget: Fusonic.Extensions.UnitTests.ServiceProvider, 9.5.0"
// Install Fusonic.Extensions.UnitTests.ServiceProvider as a Cake Addin #addin nuget:?package=Fusonic.Extensions.UnitTests.ServiceProvider&version=9.5.0 // Install Fusonic.Extensions.UnitTests.ServiceProvider as a Cake Tool #tool nuget:?package=Fusonic.Extensions.UnitTests.ServiceProvider&version=9.5.0
Unit tests
- Unit tests
Introduction
The unit test framwork tries to provide the following features & design goals:
Simplicity: The unit test base classes provide several helper methods targeted to support you using our currently used default architectural structures and libraries. To reduce reading and writing overhead (clutter) the methods tend have short, less descriptive names. For example,
Scoped
instead ofRunInSeparateLifetimeScope
, but they aren't that much and are easy to learn.- Resolving types: Can be done using
GetInstance<T>()
- Scope separation: In reality, creating data and consuming that data is not done in the same scope. In order to be able to see issues when using different scopes in the unit tests you can run your code in the provided
Scoped
andScopedAsync
methods. They run your code in separate lifetime scopes of the DI-Container. Within the tests you usually want to run your data preparations and your queries in different scopes.
- Resolving types: Can be done using
Dependency injection: The test base is layed out to be support dependency injection. Use
Fusonic.Extensions.UnitTests.ServiceProvider
to use Microsofts dependency injection, orFusonic.Extensions.UnitTests.SimpleInjector
for SimpleInjector support.Database support: The basic framework,
Fusonic.Extensions.UnitTests
does not come with any support for Databases, but we do provide support for fast and parallel database tests withFusonic.Extensions.UnitTests.EntityFrameworkCore
, currently specifically supporting PostgreSQL withFusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql
.Configuration support: Microsoft.Extensions.Configuration is supported out of a box with usable default settings.
Setup
Currently SimpleInjector and Microsofts dependency injection are supported. Reference the required library accordingly:
Fusonic.Extensions.UnitTests.SimpleInjector
orFusonic.Extensions.UnitTests.ServiceProvider
Create a TestBase
and a TestFixture
for the assembly.
The TestFixture
is used for registering your depdendencies. Override ServiceProviderTestFixture
or SimpleInjectorTestFixture
, depending on the DI container you want to use.
The TestBase
is used for tying up the test base from the extensions and your test fixture.
The setup of the TestBase
and the TestFixture
is quick and easy. In RegisterCoreDependencies
you register those dependencies that you usually need for your tests (Mediator, Services, ASP.Net services and the like). It should not be overwritten in your specific tests, so consider to make it sealed
. For your test specific fixtures use RegisterDependencies
instead.
public class TestFixture : SimpleInjectorTestFixture
{
protected sealed override void RegisterCoreDependencies(Container container)
{
//Your core dependencies here.
}
}
The test class is also abstract and requires a fixture as type parameter. Create the following base classes:
public abstract class TestBase<TFixture> : DependencyInjectionUnitTest<TFixture>
where TFixture : TestFixture
{
protected TestBase(TFixture fixture) : base(fixture)
{ }
}
public abstract class TestBase : TestBase<TestFixture>
{
protected TestBase(TestFixture fixture) : base(fixture)
{ }
// Also add common helper methods and properties to this class, eg.
/// <summary> Runs a mediator command in its own scope. Used to reduce possible side effects from test data creation and the like. </summary>
[DebuggerStepThrough]
protected Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
=> ScopedAsync(() => GetInstance<IMediator>().Send(request));
}
That's it, you're good to go. Inherit your classes from the TestBase
and get all the fancy features. If you need own fixtures, inherit them from TestFixture
and your test class from TestBase<TFixture>
. Register your test specific dependencies by overriding RegisterDependencies
.
public class FixtureForASpecificTestClass : TestFixture
{
protected override void RegisterDependencies(Container container)
{
//Your test specific fixtures here.
//No need to call the base as it does nothing.
}
}
Test settings / Microsoft.Extensions.Configuration
That's the default configuration:
public static IConfigurationBuilder ConfigureTestDefault(this IConfigurationBuilder builder, string basePath, Assembly assembly)
=> builder.SetBasePath(basePath) // basePath = Directory.GetCurrentDirectory()
.AddJsonFile("testsettings.json", optional: true)
.AddUserSecrets(assembly, optional: true) // assembly = GetType().Assembly
.AddEnvironmentVariables();
If you want to, overwrite it in your TestBase
by overwriting BuildConfiguration
. You can access the configuration with the Configuration
property. Example:
protected override void RegisterDependencies(Container container)
{
var settings = Configuration.Get<TestSettings>();
// ...
}
Database setup
For database support you can use Fusonic.Extensions.UnitTests.EntityFrameworkCore
, optionally with specific support for PostgreSQL in Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql
.
The basic idea behind those is that every test gets its own database copy. This enables parallel database testing and avoids any issues from tests affecting other tests.
Test base
A test base for your database tests is available called DatabaseUnitTest
. Type parameters are the test fixture optionally the DbContext
. Example base classes in your test lib:
public abstract class TestBase : TestBase<TestFixture>
{
protected TestBase(TestFixture fixture) : base(fixture)
{ }
}
public abstract class TestBase<TFixture> : DatabaseUnitTest<AppDbContext, TFixture>
where TFixture : TestFixture
{
protected TestBase(TFixture fixture) : base(fixture)
{ }
}
The DatabaseUnitTest
provides the methods Query
and QueryAsync
. This is basically shortcut to resolving the DbContext
and using it. So instead of writing
var count = await ScopedAsync(() =>
{
var context = GetInstance<DbContext>();
return context.Documents.CountAsync();
});
you can just use
var count = await QueryAsync(ctx => ctx.Documents.CountAsync());
QueryAsync
uses the DbContext
-type provided in DatabaseUnitTest<,>
.
If you have multiple DbContextTypes
you can always run queries for the other types with QueryAsync<TDbContext>
or Query<TDbContext>
. You can also use the DatabaseUnitTest<>
base class without TDbContext
if you do not want to use a default DbContext
-type.
PostgreSQL - Configure DbContext
A TestStore
is used for handling the test databases. For PostgreSQL, you can use the NpgsqlDatabasePerTestStore
, which creates a separate database for each test, based on a database template. You just have to pass it the connection string to the template and register it as follows:
public class TestFixture : ServiceProviderTestFixture
{
protected sealed override void RegisterCoreDependencies(ServiceCollection services)
{
var options = new NpgsqlDatabasePerTestStoreOptions
{
ConnectionString = Configuration.GetConnectionString("Npgsql")
};
var testStore = new NpgsqlDatabasePerTestStore(options);
services.AddSingleton<ITestStore>(testStore);
services.AddDbContext<AppDbContext>(b => b.UseNpgsqlDatabasePerTest(testStore));
}
}
The interface of ITestStore
is straight forward. You can easily replace your test store with something else for another strategy or for supporting other databases.
PostgreSQL - Template
When using the NpgsqlDatabasePerTest
it is assumed that you use a prepared database template. This template should have all migrations applied and may contain some seeded data. Each test gets a copy of this template. With the PostgreSqlUtil
, we provide an easy way to create such a template.
You can either create a small console application that creates the template, or do it directly once in the fixture during setup.
PostgreSQL template option: Create it in the fixture
You can create the template directly in the TestFixture by specifying a TemplateCreator
in the options:
protected sealed override void RegisterCoreDependencies(ServiceCollection services)
{
var options = new NpgsqlDatabasePerTestStoreOptions
{
ConnectionString = Configuration.GetConnectionString("Npgsql"),
TemplateCreator = CreateTemplate; // Function to create the template on first connect
};
// rest of the configuration
}
private static Task CreateTemplate(string connectionString)
=> PostgreSqlUtil.CreateTestDbTemplate<AppDbContext>(connectionString, o => new AppDbContext(o), seed: ctx => new TestDataSeed(ctx).Seed());
By default, if the template creator is set, the TestStore
checks exactly once, if the database exists.
- If the template database exists, no action will be taken. It is not checked, if the database is up to date.
- If the template database does not exist, the
TemplateCreator
is executed. - All future calls won't do anything and just return.
PostgreSqlUtil.CreateTestDbTemplate
force drops and recreates your database. However, it won't be called if the datbase already exists.
In order to get updates to your test database, either drop it or restart your postgresql container, if its data partition is mounted to tmpfs
.
You can change this behavior to always create a template by setting options.AlwaysCreateTemplate
to true. In that case, the TemplateCreator
will always be executed once per test run. This will increase the startup time for your test run though.
PostgreSQL template option: Console application
Alternatively, if you prefer to create the test database externally before the test run, create a console application with the following code in Program.cs
:
if (args.Length == 0)
{
Console.Out.WriteLine("Missing connection string.");
return 1;
}
PostgreSqlUtil.CreateTestDbTemplate<AppDbContext>(args[0], o => new AppDbContext(o), seed: ctx => new TestDataSeed(ctx).Seed());
return 0;
With that, the database given in the connection string is getting force dropped, recreated, migrations applied and optionally seeded via the given TestDataSeed
. You can simply call it in your console or the build pipeline before running the tests using
dotnet run --project <pathToCsProject> "<connectionString>"
Microsoft SQL Server - Configure DbContext
A TestStore
is used for handling the test databases. For Microsoft SQL Server, you can use the SqlServerDatabasePerTestStore
, which creates a separate database for each test. You have to pass the connection string to the database and a method to create the test database. Register it as follows:
public class TestFixture : ServiceProviderTestFixture
{
protected sealed override void RegisterCoreDependencies(ServiceCollection services)
{
var options = new SqlServerDatabasePerTestStoreOptions
{
ConnectionString = Configuration.GetConnectionString("SqlServer")!,
TemplateCreator = CreateSqlServerTemplate,
DatabasePrefix = "project_test_" // Optional. Defines a prefix for the randomly generated test database names.
DatabaseDirectoryPath = "C:/mssql/data" // Optional. Defaults to docker image default path.
};
var testStore = new SqlServerDatabasePerTestStore(options);
services.AddSingleton<ITestStore>(testStore);
services.AddDbContext<AppDbContext>(b => b.UseSqlServerDatabasePerTest(testStore));
}
private static async Task CreateSqlServerTemplate(string connectionString)
=> await SqlServerTestUtil.CreateTestDbTemplate<SqlServerDbContext>(connectionString, o => new SqlServerDbContext(o));
}
The connection string must have the Intial catalog
set. It determines the name of the template database. All tests will use a copy of the template database.
The TemplateCreator
specifies the method to create a template. It has to create and seed the database and create a backup for the copies used for the tests. Fortunately, the SqlServerTestUtil
provides a method to do exactly that.
Configuring any other database
The database support is not limited to PostgreSql and SQL Server. You just have to implement and register the ITestStore
.
For a simple example with SqLite, check Fusonic.Extensions.UnitTests.EntityFrameworkCore.Tests
→ SqliteTestStore
and TestFixture
.
Support mulitple databases in a test
You can test with multiple, different database systems at once. The setup stays basically the same, but instead of registering the test stores one by one, use the AggregateTestStore
. Example:
public class TestFixture : ServiceProviderTestFixture
{
private void RegisterDatabase(IServiceCollection services)
{
// Register Npgsql (PostgreSQL)
var npgsqlSettings = new NpgsqlDatabasePerTestStoreOptions
{
ConnectionString = Configuration.GetConnectionString("Npgsql"),
TemplateCreator = CreatePostgresTemplate
};
var npgsqlTestStore = new NpgsqlDatabasePerTestStore(npgsqlSettings);
services.AddDbContext<NpgsqlDbContext>(b => b.UseNpgsqlDatabasePerTest(npgsqlTestStore));
// Register SQL Server
var sqlServerSettings = new SqlServerDatabasePerTestStoreOptions
{
ConnectionString = Configuration.GetConnectionString("SqlServer")!,
TemplateCreator = CreateSqlServerTemplate
};
var sqlServerTestStore = new SqlServerDatabasePerTestStore(sqlServerSettings);
services.AddDbContext<SqlServerDbContext>(b => b.UseSqlServerDatabasePerTest(sqlServerTestStore));
// Combine the test stores in the AggregateTestStore
services.AddSingleton<ITestStore>(new AggregateTestStore(npgsqlTestStore, sqlServerTestStore));
}
private static Task CreatePostgresTemplate(string connectionString)
=> PostgreSqlUtil.CreateTestDbTemplate<NpgsqlDbContext>(connectionString, o => new NpgsqlDbContext(o), seed: ctx => new TestDataSeed(ctx).Seed());
private static Task CreateSqlServerTemplate(string connectionString)
=> SqlServerTestUtil.CreateTestDbTemplate<SqlServerDbContext>(connectionString, o => new SqlServerDbContext(o));
}
Database test concurrency
XUnit limits the number the maximum active tests executing, but it does not the limit of maximum parallel tests.
Simplified, as soon as a test awaits a task somewhere, the thread is returned to the pool and another test gets started. This is intended by design.
This behavior can cause issues when running integration tests against a database, especially when lots of tests are started. Connection limits can be exhausted quickly.
To solve this, you can either throttle your tests, or increase the max. connections of your test database.
To increase the max. connections of your postgres test instance, just pass the parameter max_connections. Example for a docker compose file:
postgres_test:
image: postgres:14
command: -c max_connections=300
ports:
- "5433:5432"
volumes:
- type: tmpfs
target: /var/lib/postgresql/data
- type: tmpfs
target: /dev/shm
environment:
POSTGRES_PASSWORD: developer
Alternatively, if you want to throttle your tests instead, you can to this easily with a semaphore in your test base:
public class TestBase : IAsyncLifetime
private static readonly SemaphoreSlim Throttle = new(64);
public async Task InitializeAsync() => await Throttle.WaitAsync();
public virtual Task DisposeAsync()
{
_ = Throttle.Release();
return Task.CompletedTask;
}
}
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net8.0
- Fusonic.Extensions.UnitTests (>= 9.5.0)
- Microsoft.Extensions.DependencyInjection (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
9.5.0 | 81 | 10/4/2024 |
9.4.0 | 83 | 9/23/2024 |
9.3.1 | 97 | 9/11/2024 |
9.3.0 | 100 | 9/11/2024 |
9.2.0 | 88 | 8/8/2024 |
9.2.0-rc.3 | 46 | 8/7/2024 |
9.2.0-rc.2 | 32 | 8/5/2024 |
9.2.0-rc.1 | 41 | 8/1/2024 |
9.1.0 | 68 | 7/29/2024 |
9.0.0 | 94 | 7/17/2024 |
9.0.0-preview.2 | 48 | 6/13/2024 |
9.0.0-preview.1 | 53 | 4/5/2024 |
8.1.3 | 118 | 3/27/2024 |
8.1.2 | 105 | 3/27/2024 |
8.1.1 | 116 | 3/1/2024 |
8.1.0 | 116 | 1/26/2024 |
8.0.1 | 192 | 12/18/2023 |
8.0.1-rc.2 | 84 | 12/7/2023 |
8.0.1-rc.1 | 72 | 12/6/2023 |
8.0.0 | 183 | 11/21/2023 |
8.0.0-preview1 | 68 | 10/4/2023 |
7.4.0 | 99 | 1/25/2024 |
7.3.0 | 155 | 10/11/2023 |
7.2.1-rc.1 | 88 | 8/28/2023 |
7.2.0 | 182 | 6/28/2023 |
7.2.0-alpha.1 | 78 | 6/27/2023 |
7.1.2 | 171 | 5/25/2023 |
7.1.1 | 244 | 4/3/2023 |
7.1.1-rc.2 | 80 | 3/30/2023 |
7.1.1-rc.1 | 79 | 3/30/2023 |
7.1.0 | 277 | 2/28/2023 |
7.1.0-rc.1 | 96 | 2/20/2023 |
7.0.4-rc.5 | 97 | 2/23/2023 |
7.0.4-rc.4 | 92 | 2/23/2023 |
7.0.4-rc.3 | 89 | 2/23/2023 |
7.0.4-rc.2 | 97 | 2/22/2023 |
7.0.4-rc.1 | 91 | 2/16/2023 |
7.0.3 | 282 | 2/16/2023 |
7.0.2 | 251 | 2/9/2023 |
7.0.2-rc.1 | 111 | 2/2/2023 |
7.0.1 | 422 | 1/26/2023 |
7.0.0 | 304 | 1/24/2023 |
7.0.0-beta.9 | 106 | 1/24/2023 |
7.0.0-beta.8 | 103 | 1/23/2023 |
7.0.0-beta.7 | 98 | 1/23/2023 |
7.0.0-beta.6 | 97 | 1/23/2023 |
7.0.0-beta.5 | 103 | 1/23/2023 |
7.0.0-beta.4 | 104 | 1/19/2023 |
7.0.0-beta.3 | 98 | 1/17/2023 |