diff --git a/.editorconfig b/.editorconfig index 0c86125..4827acb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -194,8 +194,8 @@ dotnet_remove_unnecessary_suppression_exclusions = none:warning # C# Unnecessary code rules [*.{cs,csx,cake}] -csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion -dotnet_diagnostic.IDE0058.severity = suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +dotnet_diagnostic.IDE0058.severity = silent csharp_style_unused_value_assignment_preference = discard_variable:suggestion dotnet_diagnostic.IDE0059.severity = suggestion diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9e5d4db..c8c471b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -36,13 +36,10 @@ test: POSTGRES_PASSWORD: postgres BACKEND_SRC_DIR: src/** SLN_PATH: Extensions.sln + ConnectionString: Host=postgres;Database=test;Username=postgres;Password=postgres environment: name: review/$CI_COMMIT_REF_SLUG url: https://fusonic.gitlab.io/-/devops/dotnet/extensions/-/jobs/${CI_JOB_ID}/artifacts/reports/dotnetcoverage/index.html - script: - - export TemplateConnectionString="Host=postgres;Database=test;Username=postgres;Password=postgres" - - dotnet run --project "src/UnitTests.EntityFrameworkCore.Npgsql/template/UnitTests.EntityFrameworkCore.Npgsql.TestDbTemplateCreator.csproj" "${TemplateConnectionString}" - - !reference [.dotnet-test, script] publish: image: mcr.microsoft.com/dotnet/sdk:7.0 @@ -54,4 +51,4 @@ publish: when: manual only: - /^release\/.*$/ - - main + - main \ No newline at end of file diff --git a/Extensions.sln b/Extensions.sln index 49b0089..eb913aa 100644 --- a/Extensions.sln +++ b/Extensions.sln @@ -39,10 +39,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosting", "src\Hosting\src\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosting.Tests", "src\Hosting\test\Hosting.Tests.csproj", "{32C689D9-1CC6-4FBD-9EF4-91BCF39B0FC1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnit.Tests", "src\XUnit\test\XUnit.Tests.csproj", "{2721A46C-6ED7-4427-BBE2-AB766D9D1FF1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnit", "src\XUnit\src\XUnit.csproj", "{E2B7EB8C-267D-426C-BB97-D4FBC5B151E9}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityFrameworkCore.Tests", "src\EntityFrameworkCore\test\EntityFrameworkCore.Tests.csproj", "{68F735B4-D3E5-477F-8071-F65D1C2137E2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests.EntityFrameworkCore.Tests", "src\UnitTests.EntityFrameworkCore\test\UnitTests.EntityFrameworkCore.Tests.csproj", "{94896282-9BBC-4373-B543-7FAC9F268695}" @@ -53,9 +49,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests.EntityFrameworkCo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests.EntityFrameworkCore.Npgsql", "src\UnitTests.EntityFrameworkCore.Npgsql\src\UnitTests.EntityFrameworkCore.Npgsql.csproj", "{4989F141-4A89-4CE0-80E2-3EA4170552D3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests.EntityFrameworkCore.Npgsql.TestDbTemplateCreator", "src\UnitTests.EntityFrameworkCore.Npgsql\template\UnitTests.EntityFrameworkCore.Npgsql.TestDbTemplateCreator.csproj", "{1359BBC2-7C58-42F1-8E92-BF073CE44208}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests.SimpleInjector", "src\UnitTests.SimpleInjector\src\UnitTests.SimpleInjector.csproj", "{0FA88398-9B9F-4C41-92B8-08CBF99F249A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityFrameworkCore.Abstractions", "src\EntityFrameworkCore.Abstractions\src\EntityFrameworkCore.Abstractions.csproj", "{E292E274-10B4-4ACE-B822-C526CCF01568}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests.ServiceProvider", "src\UnitTests.ServiceProvider\src\UnitTests.ServiceProvider.csproj", "{3624084E-62DA-40BD-8A5B-C933FA3151BB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -115,14 +111,6 @@ Global {32C689D9-1CC6-4FBD-9EF4-91BCF39B0FC1}.Debug|Any CPU.Build.0 = Debug|Any CPU {32C689D9-1CC6-4FBD-9EF4-91BCF39B0FC1}.Release|Any CPU.ActiveCfg = Release|Any CPU {32C689D9-1CC6-4FBD-9EF4-91BCF39B0FC1}.Release|Any CPU.Build.0 = Release|Any CPU - {2721A46C-6ED7-4427-BBE2-AB766D9D1FF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2721A46C-6ED7-4427-BBE2-AB766D9D1FF1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2721A46C-6ED7-4427-BBE2-AB766D9D1FF1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2721A46C-6ED7-4427-BBE2-AB766D9D1FF1}.Release|Any CPU.Build.0 = Release|Any CPU - {E2B7EB8C-267D-426C-BB97-D4FBC5B151E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E2B7EB8C-267D-426C-BB97-D4FBC5B151E9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E2B7EB8C-267D-426C-BB97-D4FBC5B151E9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E2B7EB8C-267D-426C-BB97-D4FBC5B151E9}.Release|Any CPU.Build.0 = Release|Any CPU {68F735B4-D3E5-477F-8071-F65D1C2137E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {68F735B4-D3E5-477F-8071-F65D1C2137E2}.Debug|Any CPU.Build.0 = Debug|Any CPU {68F735B4-D3E5-477F-8071-F65D1C2137E2}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -143,14 +131,14 @@ Global {4989F141-4A89-4CE0-80E2-3EA4170552D3}.Debug|Any CPU.Build.0 = Debug|Any CPU {4989F141-4A89-4CE0-80E2-3EA4170552D3}.Release|Any CPU.ActiveCfg = Release|Any CPU {4989F141-4A89-4CE0-80E2-3EA4170552D3}.Release|Any CPU.Build.0 = Release|Any CPU - {1359BBC2-7C58-42F1-8E92-BF073CE44208}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1359BBC2-7C58-42F1-8E92-BF073CE44208}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1359BBC2-7C58-42F1-8E92-BF073CE44208}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1359BBC2-7C58-42F1-8E92-BF073CE44208}.Release|Any CPU.Build.0 = Release|Any CPU - {E292E274-10B4-4ACE-B822-C526CCF01568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E292E274-10B4-4ACE-B822-C526CCF01568}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E292E274-10B4-4ACE-B822-C526CCF01568}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E292E274-10B4-4ACE-B822-C526CCF01568}.Release|Any CPU.Build.0 = Release|Any CPU + {0FA88398-9B9F-4C41-92B8-08CBF99F249A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0FA88398-9B9F-4C41-92B8-08CBF99F249A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0FA88398-9B9F-4C41-92B8-08CBF99F249A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0FA88398-9B9F-4C41-92B8-08CBF99F249A}.Release|Any CPU.Build.0 = Release|Any CPU + {3624084E-62DA-40BD-8A5B-C933FA3151BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3624084E-62DA-40BD-8A5B-C933FA3151BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3624084E-62DA-40BD-8A5B-C933FA3151BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3624084E-62DA-40BD-8A5B-C933FA3151BB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -161,11 +149,9 @@ Global {4443416D-35A0-48B0-AA92-785FECFDE582} = {95D2383B-80C5-46AA-9190-826A0377A156} {C0FDFA5A-5421-40A1-8653-823FB9C94E5C} = {95D2383B-80C5-46AA-9190-826A0377A156} {32C689D9-1CC6-4FBD-9EF4-91BCF39B0FC1} = {95D2383B-80C5-46AA-9190-826A0377A156} - {2721A46C-6ED7-4427-BBE2-AB766D9D1FF1} = {95D2383B-80C5-46AA-9190-826A0377A156} {68F735B4-D3E5-477F-8071-F65D1C2137E2} = {95D2383B-80C5-46AA-9190-826A0377A156} {94896282-9BBC-4373-B543-7FAC9F268695} = {95D2383B-80C5-46AA-9190-826A0377A156} {86B0E8DD-25F8-46DF-95D5-781ECC1BCB13} = {95D2383B-80C5-46AA-9190-826A0377A156} - {1359BBC2-7C58-42F1-8E92-BF073CE44208} = {95D2383B-80C5-46AA-9190-826A0377A156} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DCAD7035-5EC8-4B6E-82B5-769C5A4892EA} diff --git a/README.md b/README.md index 597586a..472f93d 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,6 @@ Adds support for sending emails. See the [documentation](docs/Email/README.md) f [![NuGet](https://img.shields.io/nuget/v/Fusonic.Extensions.EntityFrameworkCore.svg?label=Fusonic.Extensions.EntityFrameworkCore&style=plastic)](https://www.nuget.org/packages/Fusonic.Extensions.EntityFrameworkCore/) Extensions for EF Core. -[![NuGet](https://img.shields.io/nuget/v/Fusonic.Extensions.EntityFrameworkCore.Abstractions.svg?label=Fusonic.Extensions.EntityFrameworkCore.Abstractions&style=plastic)](https://www.nuget.org/packages/Fusonic.Extensions.EntityFrameworkCore.Abstractions/) -Abstractions to the EF Core extensions without referencing EF Core. - [![NuGet](https://img.shields.io/nuget/v/Fusonic.Extensions.Hangfire.svg?label=Fusonic.Extensions.Hangfire&style=plastic)](https://www.nuget.org/packages/Fusonic.Extensions.Hangfire/) Provides Hangfire extensions, especially suited for CQRS developement. (Out of band processing). See the [documentation](docs/Hangfire/README.md) for more details. @@ -43,7 +40,13 @@ Provides services and extensions for hosting. See the [documentation](docs/Hosti Provides abstractions for MediatR. See the [documentation](docs/MediatR/README.md) for more details. [![NuGet](https://img.shields.io/nuget/v/Fusonic.Extensions.UnitTests.svg?label=Fusonic.Extensions.UnitTests&style=plastic)](https://www.nuget.org/packages/Fusonic.Extensions.UnitTests/) -Xunit-based testing base classes. Supports DI with SimpleInjector, MediatR-event-recordings, Lifetime-Scoped calls, a test context and so on. See the [unit test documentation](docs/UnitTests/README.md) for more details. +Xunit-based testing base classes with support for dependency injection. Libraries supporting specific DI containers (SimpleInjector, ServiceProvider) are in separate packages. See the [unit test documentation](docs/UnitTests/README.md) for more details. + +[![NuGet](https://img.shields.io/nuget/v/Fusonic.Extensions.UnitTests.ServiceProvider.svg?label=Fusonic.Extensions.UnitTests.ServiceProvider&style=plastic)](https://www.nuget.org/packages/Fusonic.Extensions.UnitTests.ServiceProvider/) +Xunit-based testing base classes. Supports dependency injection with Microsofts Dependency Injection framework (ServiceProvider).. See the [unit test documentation](docs/UnitTests/README.md) for more details. + +[![NuGet](https://img.shields.io/nuget/v/Fusonic.Extensions.UnitTests.SimpleInjector.svg?label=Fusonic.Extensions.UnitTests.SimpleInjector&style=plastic)](https://www.nuget.org/packages/Fusonic.Extensions.UnitTests.SimpleInjector/) +Xunit-based testing base classes. Supports dependency injection with SimpleInjector.. See the [unit test documentation](docs/UnitTests/README.md) for more details. [![NuGet](https://img.shields.io/nuget/v/Fusonic.Extensions.UnitTests.EntityFrameworkCore.svg?label=Fusonic.Extensions.UnitTests.EntityFrameworkCore&style=plastic)](https://www.nuget.org/packages/Fusonic.Extensions.UnitTests.EntityFrameworkCore/) Adds database support using EF Core to the unit tests. See the [unit test documentation](docs/UnitTests/README.md) for more details. @@ -51,9 +54,6 @@ Adds database support using EF Core to the unit tests. See the [unit test docume [![NuGet](https://img.shields.io/nuget/v/Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.svg?label=Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql&style=plastic)](https://www.nuget.org/packages/Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql/) Adds support for database tests using EF Core with PostgreSQL. See the [unit test documentation](docs/UnitTests/README.md) for more details. -[![NuGet](https://img.shields.io/nuget/v/Fusonic.Extensions.XUnit.svg?label=Fusonic.Extensions.XUnit&style=plastic)](https://www.nuget.org/packages/Fusonic.Extensions.XUnit/) -Extensions to XUnit. Provides an own framework adding capabilities like a test context and attributes running before a test class gets instantiated. - Important information =============== diff --git a/docs/UnitTests/README.md b/docs/UnitTests/README.md index f2897c4..98d49bb 100644 --- a/docs/UnitTests/README.md +++ b/docs/UnitTests/README.md @@ -3,50 +3,47 @@ - [Unit tests](#unit-tests) - [Introduction](#introduction) - [Setup](#setup) - - [Test scoped lifestyle](#test-scoped-lifestyle) - [Test settings / Microsoft.Extensions.Configuration](#test-settings--microsoftextensionsconfiguration) - [Database setup](#database-setup) - [Test base](#test-base) + - [PostgreSQL - Configure DbContext](#postgresql---configure-dbcontext) - [PostgreSQL - Template](#postgresql---template) - - [PostgreSQL - Configure single DbContext](#postgresql---configure-single-dbcontext) - - [PostgreSQL - Configure multiple DbContexts with same database](#postgresql---configure-multiple-dbcontexts-with-same-database) - - [PostgreSQL - Configure multiple DbContexts with different databases](#postgresql---configure-multiple-dbcontexts-with-different-databases) + - [PostgreSQL template option: Create it in the fixture](#postgresql-template-option-create-it-in-the-fixture) + - [PostgreSQL template option: Console application](#postgresql-template-option-console-application) - [Configuring any other database](#configuring-any-other-database) - [Database test concurrency](#database-test-concurrency) - - [Samples](#samples) ## 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 of `RunInSeparateLifetimeScope`, but they aren't that much and are easy to learn. - - **Resolving types:** Can be done via the Container property or dirctly using `GetInstance()` - - **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` and `ScopedAsync` methods. They run your code in separate lifetime scopes of the SimpleInjector-Container. Within the tests you usually want to run your data preparations and your queries in different scopes. + - **Resolving types:** Can be done using `GetInstance()` + - **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` and `ScopedAsync` 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. - **MediatR support:** Send your MediatR-requests in a separate scope by simply calling `SendAsync`. -- **Dependency injection:** The test base is layed out to be used with SimpleInjector as dependency injection framework. Services can be directly resolved from the test class. (Constructor injection is not supported, but could be easily extended if required.) - - **Dependencies per test:** Most services are fine to be resolved within the scopes provided by SimpleInjector. However, some unit test framework services depend on the test method (`DatabaseProvider`) or require one instance to span multiple scopes in one test. You can register such dependencies by using the `TestScopedLifestyle` from the fixture. +- **Dependency injection:** The test base is layed out to be support dependency injection. Use `Fusonic.Extensions.UnitTests.ServiceProvider` to use Microsofts dependency injection, or `Fusonic.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 with `Fusonic.Extensions.UnitTests.EntityFrameworkCore`, currently specifically supporting PostgreSQL with `Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql`. - **Configuration support:** Microsoft.Extensions.Configuration is supported out of a box with usable default settings. -- **TestContext:** Useful for framework code, the test context provides info about the executing method and class and access to the test output helper. You can also put own objects into the context that span the lifetime of the test. - ## Setup -The intention is that you have one base class and one base fixture per test assembly. You don't have multiple classes for use cases where the only difference is a resolved service. +Currently SimpleInjector and Microsofts dependency injection are supported. Reference the required library accordingly: +- `Fusonic.Extensions.UnitTests.SimpleInjector` or +- `Fusonic.Extensions.UnitTests.ServiceProvider` -To activate the features of the lib (like the `TestContext` and the per-test-database) you need to create an `AssemblyInfo.cs` with the following contents: +Create a `TestBase` and a `TestFixture` for the assembly. -```cs -[assembly: Fusonic.Extensions.UnitTests.XunitExtensibility.FusonicTestFramework] -``` +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 (MediatR, 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. ```cs -public class TestFixture : UnitTestFixture +public class TestFixture : SimpleInjectorTestFixture { protected sealed override void RegisterCoreDependencies(Container container) { @@ -58,7 +55,7 @@ public class TestFixture : UnitTestFixture The test class is also abstract and requires a fixture as type parameter. Create the following base classes: ```cs -public abstract class TestBase : UnitTest +public abstract class TestBase : DependencyInjectionUnitTest where TFixture : TestFixture { protected TestBase(TFixture fixture) : base(fixture) @@ -72,7 +69,7 @@ public abstract class TestBase : TestBase } ``` -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`. Register your dependencies by overriding `RegisterDependencies`. +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`. Register your test specific dependencies by overriding `RegisterDependencies`. ```cs public class FixtureForASpecificTestClass : TestFixture @@ -85,49 +82,25 @@ public class FixtureForASpecificTestClass : TestFixture } ``` -## Test scoped lifestyle - -There is a lifetime scope available that is valid within one test. For example, the `DatabaseProvider` needs to be one instance per test and a new instance on the next. - -If you have such a requirement, register your serivce with the `TestScopedLifestyle`: - -```cs -protected override void RegisterDependencies(Container container) -{ - //Note: the usual overrides for the register methods are available too, - //like RegisterTestScoped() and RegisterTestScoped(Func) - container.RegisterTestScoped(); -} -``` - ## Test settings / Microsoft.Extensions.Configuration That's the default configuration: ```cs -public static IConfiguration GetDefaultConfiguration(string basePath, Assembly assembly) -{ - return new ConfigurationBuilder() - .SetBasePath(basePath) - .AddJsonFile("testsettings.json", optional: true) - .AddUserSecrets(assembly, optional: true) - .AddEnvironmentVariables() - .Build(); -} - -protected virtual IConfiguration BuildConfiguration() => GetDefaultConfiguration(Directory.GetCurrentDirectory(), GetType().Assembly); +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 also bind your test settings objects like this: +If you want to, overwrite it in your `TestBase` by overwriting `BuildConfiguration`. You can access the configuration with the `Configuration` property. Example: ```cs -public TestSettings TestSettings { get; } = new TestSettings(); - -protected override IConfiguration BuildConfiguration() +protected override void RegisterDependencies(Container container) { - var configuration = base.BuildConfiguration(); - configuration.Bind(TestSettings); - return configuration; + var settings = Configuration.Get(); + // ... } ``` @@ -152,14 +125,10 @@ public abstract class TestBase : DatabaseUnitTest GetInstance().DropDatabase(); } ``` -Note the `DropTestDatabase`: This method gets called when a test completes (successfully or unsuccessfully). The database for the test gets dropped then, as it is not required anymore. If you do not wish to use the `DatabaseUnitTest`-base, you have to take care about cleanup yourselfes. - -Furhtermore, the `DatabaseUnitTest` provides the methods `Query` and `QueryAsync`. This is basically shortcut to resolving the `DbContext` and using it. So instead of writing +The `DatabaseUnitTest` provides the methods `Query` and `QueryAsync`. This is basically shortcut to resolving the `DbContext` and using it. So instead of writing ```cs var count = await ScopedAsync(() => @@ -177,132 +146,122 @@ var count = await ScopedAsync(() => If you have multiple `DbContextTypes` you can always run queries for the other types with `QueryAsync` or `Query`. You can also use the `DatabaseUnitTest<>` base class without `TDbContext` if you do not want to use a default `DbContext`-type. -### PostgreSQL - Template +### PostgreSQL - Configure DbContext -For PostgreSQL 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. Create a console application with the following code in `Program.cs`: +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: ```cs -if (args.Length == 0) +public class TestFixture : ServiceProviderTestFixture { - Console.Out.WriteLine("Missing connection string."); - return 1; -} - -PostgreSqlUtil.CreateTestDbTemplate(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 -```sh -dotnet run --project "" -``` - -### PostgreSQL - Configure single DbContext - -In case you only have one `DbContext`, you can simply configure the database tests like follows: - -```cs -public class TestFixture : UnitTestFixture -{ - protected sealed override void RegisterCoreDependencies(Container container) + protected sealed override void RegisterCoreDependencies(ServiceCollection services) { - container.RegisterNpgsqlDbContext(Configuration.Bind); + var options = Configuration.Get(); + var testStore = new NpgsqlDatabasePerTestStore(options); + services.AddSingleton(testStore); + + services.AddDbContext(b => b.UseNpgsqlDatabasePerTest(testStore)); } } ``` -`RegisterNpgsqlDbContext` registers your `DbContext` and all services for triggering the creation and deletion of the test databases when required. +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 -**Settings:** -The settings bound via `Configuration.Bind` are the `NpgsqlTestDatabaseProviderSettings`, which require the connection string to the test database template in the setting `TemplateConnectionString`. Provide this setting via an environment variable and/or using a `testsettings.json`. +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. -That's it. Each of your tests now gets a fresh database, if required. It does not unnecessarily create databases, if there is no database access with in a test. +You can either create a small console application that creates the template, or do it directly once in the fixture during setup. -### PostgreSQL - Configure multiple DbContexts with same database +#### PostgreSQL template option: Create it in the fixture -If you have for example a DbContext for writing and another one for reading (usually with a different connection string in the deployed application), you might -want to register them using the same connection string in the tests, as you usually don't start multpile replicated PostgreSQL-servers for testing. +You can create the template directly in the TestFixture by specifying a `TemplateCreator` in the options: ```cs -public class TestFixture : UnitTestFixture +protected sealed override void RegisterCoreDependencies(ServiceCollection services) { - protected sealed override void RegisterCoreDependencies(Container container) - { - container.RegisterNpgsqlDbContext(Configuration.Bind); - container.RegisterNpgsqlDbContext(Configuration.Bind); - } + var options = Configuration.Get(); + options.TemplateCreator = CreateTemplate; // Function to create the template on first connect + + // rest of the configuration } + +private static Task CreateTemplate(string connectionString) + => PostgreSqlUtil.CreateTestDbTemplate(connectionString, o => new AppDbContext(o), seed: ctx => new TestDataSeed(ctx).Seed()); ``` -Both DbContexts get the same connection string. In your test base you might want to override `DatabaseUnitTests` using your write-DbContext as default for easier test data setup. +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. -Note: If you configure the `ReadOnlyDbContext` with a different connection string, in this scenario you'd overwrite the configuration of `AppDbContext`. +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`. -### PostgreSQL - Configure multiple DbContexts with different databases +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. -If you have multpile DbContexts requiring different database connections, you need to create overwrites for the database provider and its settings. The `NpgsqlTestDatabaseProvider` is responsible for creating and deleting the test databases that are provided for each test and used as default when you call `RegisterNpgsqlDbContext` with one type parameter. +#### 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`: ```cs -public class TestFixture : UnitTestFixture +if (args.Length == 0) { - protected sealed override void RegisterCoreDependencies(Container container) - { - container.RegisterNpgsqlDbContext(Configuration.Bind); - container.RegisterNpgsqlDbContext(Configuration.GetSection("OtherDatabase").Bind); - } + Console.Out.WriteLine("Missing connection string."); + return 1; } -public class OtherNpgsqlTestDatabaseProvider : NpgsqlTestDatabaseProvider -{ - public OtherNpgsqlTestDatabaseProvider(OtherNpgsqlTestDatabaseProviderSettings settings) : base(settings) - { } -} -public class OtherNpgsqlTestDatabaseProviderSettings : NpgsqlTestDatabaseProviderSettings { } -``` +PostgreSqlUtil.CreateTestDbTemplate(args[0], o => new AppDbContext(o), seed: ctx => new TestDataSeed(ctx).Seed()); -Now both contexts get different connection strings (via the `NpgsqlTestDatabaseProviderSettings`) and their own test database. +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 +```sh +dotnet run --project "" +``` ### Configuring any other database -The database support is not limited to PostgreSql, `RegisterNpgsqlDbContext` is just further calling `RegisterDbContext` from `Fusonic.Extensions.UnitTests.EntityFrameworkCore`. - -For supporting other databases, you just need to implement the `ITestDatabaseProvider`. It is responsible for -- Supplying a test database name (usually unique per test) -- Create the test database with that name -- Drop the test database with that name - -With that done, you can just use `container.RegisterDbContext(...)` to fully support your database. +The database support is not limited to PostgreSql. You just have to implement and register the `ITestStore`. -For a simple example with SqLite, check `Fusonic.Extensions.UnitTests.EntityFrameworkCore.Tests` -> `TestDatabaseProvider` and `TestFixture`. +For a simple example with SqLite, check `Fusonic.Extensions.UnitTests.EntityFrameworkCore.Tests` -> `SqliteTestStore` and `TestFixture`. ### 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 and other issues, like timeouts due to overload, may occur. - -Our framework provides a possibility to limit the max. number of tests executing in parallel. This can be done in two ways: -* Set `MaxParallelTests` on the `FusonicTestFramework` assembly attribute, or -* set the environment variable `MAX_PARALLEL_TESTS` - -If both are set, the environment variable has the higher precedence. - -Note that this setting is not affecting the connection limit of entity framework or any other connection limits. Entity framework or this unit testing framework could still have more open connections than the MaxParallelTests setting, but it still can be leveraged to drastically reduce the chance of connection limit exhaustion and timeouts due to a too high load. +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: +```yaml +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 +``` -The following values are supported: -* `maxParallelTests < 0` disables the limits completly. Tests get executed as XUnit intends to. -* `maxParallelTests = 0` sets the limit to the virtual processor count of the machine. This is the default. -* `maxParallelTests > 0` sets the limit to the given number. +Alternatively, if you want to throttle your tests instead, you can to this easily with a semaphore in your test base: -## Samples +```cs +public class TestBase : IAsyncLifetime -There's a series of blog posts released about how to do fast unit testing, also related to this library. It is split into three parts: - - [Fast unit tests with databases, part 1 – a primer](https://www.fusonic.net/de/blog/fast-unit-tests-with-databases-part-1) - - [Fast unit tests with databases, part 2 – Introduction to the fusonic testing framework](https://www.fusonic.net/de/blog/fast-unit-tests-with-databases-part-2) - - [Fast unit tests with databases, part 3 – Implementation of our solution](https://www.fusonic.net/de/blog/fast-unit-tests-with-databases-part-3) + private static readonly SemaphoreSlim Throttle = new(64); + public async Task InitializeAsync() => await Throttle.WaitAsync(); -Related to that there's a [sample project in this repository](../../samples/UnitTests/). + public virtual Task DisposeAsync() + { + _ = Throttle.Release(); + return Task.CompletedTask; + } +} +``` diff --git a/samples/UnitTests/.gitignore b/samples/UnitTests/.gitignore deleted file mode 100644 index 2afa2e2..0000000 --- a/samples/UnitTests/.gitignore +++ /dev/null @@ -1,454 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET -project.lock.json -project.fragment.lock.json -artifacts/ - -# Tye -.tye/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -## -## Visual studio for Mac -## - - -# globs -Makefile.in -*.userprefs -*.usertasks -config.make -config.status -aclocal.m4 -install-sh -autom4te.cache/ -*.tar.gz -tarballs/ -test-results/ - -# Mac bundle stuff -*.dmg -*.app - -# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore -# Windows thumbnail cache files -Thumbs.db -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# JetBrains Rider -.idea/ -*.sln.iml - -## -## Visual Studio Code -## -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json diff --git a/samples/UnitTests/1_run_postgres.cmd b/samples/UnitTests/1_run_postgres.cmd deleted file mode 100644 index 7b64335..0000000 --- a/samples/UnitTests/1_run_postgres.cmd +++ /dev/null @@ -1,3 +0,0 @@ -@echo off - -docker-compose -f docker/postgres.yml up \ No newline at end of file diff --git a/samples/UnitTests/1_run_postgres.sh b/samples/UnitTests/1_run_postgres.sh deleted file mode 100755 index 20d08a4..0000000 --- a/samples/UnitTests/1_run_postgres.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -docker-compose -f docker/postgres.yml up \ No newline at end of file diff --git a/samples/UnitTests/2_create_testdb.cmd b/samples/UnitTests/2_create_testdb.cmd deleted file mode 100644 index c0f3639..0000000 --- a/samples/UnitTests/2_create_testdb.cmd +++ /dev/null @@ -1,5 +0,0 @@ -@echo off - -dotnet build src/Example.sln -dotnet tool install -g Fusonic.Extensions.UnitTests.Tools.PostgreSql -pgtestutil template -c "Host=127.0.0.1;Port=5433;Database=example_test;Username=postgres;Password=postgres" -a "src/Example.Database.Tests/bin/Debug/net6.0/Example.Database.Tests.dll" \ No newline at end of file diff --git a/samples/UnitTests/2_create_testdb.sh b/samples/UnitTests/2_create_testdb.sh deleted file mode 100755 index f6d3cf8..0000000 --- a/samples/UnitTests/2_create_testdb.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -dotnet build src/Example.sln -dotnet tool install -g Fusonic.Extensions.UnitTests.Tools.PostgreSql -pgtestutil template -c "Host=127.0.0.1;Port=5433;Database=example_test;Username=postgres;Password=postgres" -a "src/Example.Database.Tests/bin/Debug/net6.0/Example.Database.Tests.dll" \ No newline at end of file diff --git a/samples/UnitTests/3_run_tests.cmd b/samples/UnitTests/3_run_tests.cmd deleted file mode 100644 index f2a02fe..0000000 --- a/samples/UnitTests/3_run_tests.cmd +++ /dev/null @@ -1,5 +0,0 @@ -@echo off - -dotnet test src/Example.sln - -pause \ No newline at end of file diff --git a/samples/UnitTests/3_run_tests.sh b/samples/UnitTests/3_run_tests.sh deleted file mode 100755 index e169cf7..0000000 --- a/samples/UnitTests/3_run_tests.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -dotnet test src/Example.sln \ No newline at end of file diff --git a/samples/UnitTests/README.md b/samples/UnitTests/README.md deleted file mode 100644 index 4fbf4fb..0000000 --- a/samples/UnitTests/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Fusonic blog example -Example for the blog post [Fast unit tests with databases](https://www.fusonic.net/de/blog/fast-unit-tests-with-databases-part-1) on [fusonic.net](https://fusonic.net). - -To get the tests running, you need at least -- .NET6 SDK -- Docker 20.10 - -Then start -- `1_run_postgres`, which starts a postgres server in docker -- `2_create_testdb`, which creates a test database template on the server - -You can either run the tests directly from your IDE (VisualStudio, VS Code, Rider, ...) or run them using the `3_run_tests` script. - -Note: -When running the tests, the postgres service logs tons of error messages - -FATAL: terminating connection due to administrator command - -This is fine. The tests clean up after them and force drop the test database when finished. \ No newline at end of file diff --git a/samples/UnitTests/docker/postgres.yml b/samples/UnitTests/docker/postgres.yml deleted file mode 100644 index e310096..0000000 --- a/samples/UnitTests/docker/postgres.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: '3.8' - -services: - postgres_test: - image: postgres:13 - ports: - - 5433:5432 - volumes: - - type: tmpfs - target: /var/lib/postgresql/data - - type: tmpfs - target: /dev/shm - environment: - POSTGRES_PASSWORD: postgres \ No newline at end of file diff --git a/samples/UnitTests/src/Directory.Build.props b/samples/UnitTests/src/Directory.Build.props deleted file mode 100644 index 4af1e50..0000000 --- a/samples/UnitTests/src/Directory.Build.props +++ /dev/null @@ -1,7 +0,0 @@ - - - net6.0 - enable - enable - - \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Database.Tests/Example.Database.Tests.csproj b/samples/UnitTests/src/Example.Database.Tests/Example.Database.Tests.csproj deleted file mode 100644 index 6d1fff7..0000000 --- a/samples/UnitTests/src/Example.Database.Tests/Example.Database.Tests.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - false - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - PreserveNewest - - - - diff --git a/samples/UnitTests/src/Example.Database.Tests/PersonServiceTests.cs b/samples/UnitTests/src/Example.Database.Tests/PersonServiceTests.cs deleted file mode 100644 index 24861db..0000000 --- a/samples/UnitTests/src/Example.Database.Tests/PersonServiceTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Example.Database.Domain; -using FluentAssertions; -using Fusonic.Extensions.UnitTests.Adapters.PostgreSql; - -namespace Example.Database.Tests; - -// Note: It is assumed that you have docker and the postgres service from the root directory of this example is running -// You can launch it using one of the run_postgres scripts. It starts a PostgreSQL server on port 5433 having -// the data folder mounted to an InMemory (tmpfs) volume. -public class PersonServiceTests : TestBase -{ - public PersonServiceTests(TestFixture fixture) : base(fixture) - { } - - [Fact] - [PostgreSqlTest] - public async Task ThisTestAlwaysUsesAPostgresDatabase() - { - // Arrange - await QueryAsync(async ctx => - { - ctx.Add(new Person("Henry")); - await ctx.SaveChangesAsync(); - }); - - // Act - var result = await ScopedAsync(() => GetInstance().GetPersons()); - - // Assert - result.Should().HaveCount(2) // second one is from the seed - .And.Contain(p => p.Name == "Henry"); - } - - [Fact] - public async Task ThisTestRunsInMemoryByDefault() - { - // Arrange - await QueryAsync(async ctx => - { - ctx.Add(new Person("Henry2")); - await ctx.SaveChangesAsync(); - }); - - // Act - var result = await ScopedAsync(() => GetInstance().GetPersons()); - - // Assert - result.Should().HaveCount(2) // second one is from the seed - .And.Contain(p => p.Name == "Henry2"); - } - - // This test is executed 1500 times. Each test runs against the PostgreSQL server and each test gets its own database. - // Having the servers data directory mounted to an InMemory (tmpfs) volume, this should still be very fast. - [Theory] - [MemberData(nameof(ALotOfTestData))] - [PostgreSqlTest] - public async Task ALotOfDatabaseTests(int postfix) - { - // Arrange - await QueryAsync(async ctx => - { - ctx.Add(new Person("Henry " + postfix)); - await ctx.SaveChangesAsync(); - }); - - // Act - var result = await ScopedAsync(() => GetInstance().GetPersons()); - - // Assert - result.Should().HaveCount(2) // second one is from the seed - .And.Contain(p => p.Name == "Henry " + postfix); - } - - public static IEnumerable ALotOfTestData => Enumerable.Range(1, 1500) - .Select(i => new object[] { i }); -} \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Database.Tests/Properties/AssemblyInfo.cs b/samples/UnitTests/src/Example.Database.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 2add585..0000000 --- a/samples/UnitTests/src/Example.Database.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using Fusonic.Extensions.XUnit.Framework; - -[assembly:FusonicTestFramework] \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Database.Tests/TestBase.cs b/samples/UnitTests/src/Example.Database.Tests/TestBase.cs deleted file mode 100644 index 74fe0c1..0000000 --- a/samples/UnitTests/src/Example.Database.Tests/TestBase.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Example.Database.Data; -using Fusonic.Extensions.UnitTests.Adapters.EntityFrameworkCore; - -namespace Example.Database.Tests; - -public class TestBase : TestBase -{ - public TestBase(TestFixture fixture) : base(fixture) - { } -} - -public class TestBase : DatabaseUnitTest - where TFixture : TestFixture -{ - public TestBase(TFixture fixture) : base(fixture) - { } -} \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Database.Tests/TestDataSeed.cs b/samples/UnitTests/src/Example.Database.Tests/TestDataSeed.cs deleted file mode 100644 index 7f674fb..0000000 --- a/samples/UnitTests/src/Example.Database.Tests/TestDataSeed.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Example.Database.Data; -using Example.Database.Domain; - -namespace Example.Database.Tests; - -public class TestDataSeed -{ - private readonly AppDbContext dbContext; - public TestDataSeed(AppDbContext dbContext) => this.dbContext = dbContext; - - public async Task Seed() - { - dbContext.Add(new Person("Olaf")); - await dbContext.SaveChangesAsync(); - } -} \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Database.Tests/TestDbTemplateCreator.cs b/samples/UnitTests/src/Example.Database.Tests/TestDbTemplateCreator.cs deleted file mode 100644 index 48ef413..0000000 --- a/samples/UnitTests/src/Example.Database.Tests/TestDbTemplateCreator.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Example.Database.Data; -using Fusonic.Extensions.UnitTests.Adapters.EntityFrameworkCore; -using Fusonic.Extensions.UnitTests.Adapters.PostgreSql; - -namespace Example.Database.Tests; - -public class TestDbTemplateCreator : ITestDbTemplateCreator -{ - public void Create(string connectionString) - { - //The connection string contains the test db name that gets used as prefix. - var dbName = PostgreSqlUtil.GetDatabaseName(connectionString); - - //Drop all databases that may still be there from previously stopped tests. - PostgreSqlUtil.Cleanup(connectionString, dbPrefix: dbName!); - - //Create the template - PostgreSqlUtil.CreateTestDbTemplate(connectionString, o => new AppDbContext(o), seed: c => new TestDataSeed(c).Seed()); - } -} \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Database.Tests/TestFixture.cs b/samples/UnitTests/src/Example.Database.Tests/TestFixture.cs deleted file mode 100644 index 80c2cce..0000000 --- a/samples/UnitTests/src/Example.Database.Tests/TestFixture.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Example.Database.Data; -using Fusonic.Extensions.UnitTests.Adapters.EntityFrameworkCore; -using Fusonic.Extensions.UnitTests.Adapters.InMemoryDatabase; -using Fusonic.Extensions.UnitTests.Adapters.PostgreSql; -using Microsoft.Extensions.Configuration; -using SimpleInjector; - -namespace Example.Database.Tests; - -public class TestFixture : DatabaseFixture -{ - public TestSettings TestSettings { get; } = new(); - - protected override IConfiguration BuildConfiguration() - { - var configuration = base.BuildConfiguration(); - configuration.Bind(TestSettings); - return configuration; - } - - protected override void ConfigureDatabaseProviders(DatabaseFixtureConfiguration configuration) - { - // With a postgres having an InMemory data volume you usually don't want this. It's only for demonstration purposes. - configuration.UsePostgreSqlDatabase(TestSettings.ConnectionString, TestSettings.TestDbPrefix, TestSettings.TestDbTemplate) - .UseInMemoryDatabase(seed: ctx => new TestDataSeed(ctx).Seed()) - .UseDefaultProviderAttribute(new InMemoryTestAttribute()); - - if (bool.TryParse(Environment.GetEnvironmentVariable("NIGHTLY"), out var isNightly) && isNightly) - configuration.UseProviderAttributeReplacer(_ => new PostgreSqlTestAttribute()); - - // With a postgres having an InMemory data volume you usually would have this configuration: - // configuration.UsePostgreSqlDatabase(TestSettings.ConnectionString, TestSettings.TestDbPrefix, TestSettings.TestDbTemplate) - // .UseDefaultProviderAttribute(new PostgreSqlTestAttribute()); - } - - protected sealed override void RegisterCoreDependencies(Container container) - { - base.RegisterCoreDependencies(container); - container.Register(); - } -} \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Database.Tests/TestSettings.cs b/samples/UnitTests/src/Example.Database.Tests/TestSettings.cs deleted file mode 100644 index 1e9c93b..0000000 --- a/samples/UnitTests/src/Example.Database.Tests/TestSettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Example.Database.Tests; - -public class TestSettings -{ - public string ConnectionString { get; set; } = null!; - public string TestDbPrefix { get; set; } = null!; - public string TestDbTemplate { get; set; } = null!; -} \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Database.Tests/Usings.cs b/samples/UnitTests/src/Example.Database.Tests/Usings.cs deleted file mode 100644 index 8c927eb..0000000 --- a/samples/UnitTests/src/Example.Database.Tests/Usings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Xunit; \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Database.Tests/testsettings.json b/samples/UnitTests/src/Example.Database.Tests/testsettings.json deleted file mode 100644 index 5203bc8..0000000 --- a/samples/UnitTests/src/Example.Database.Tests/testsettings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "ConnectionString": "Host=127.0.0.1;Port=5433;Database=example_test;Username=postgres;Password=postgres", - "TestDbPrefix": "example_test", - "TestDbTemplate": "example_test" -} diff --git a/samples/UnitTests/src/Example.Database/Data/AppDbContext.cs b/samples/UnitTests/src/Example.Database/Data/AppDbContext.cs deleted file mode 100644 index a668f87..0000000 --- a/samples/UnitTests/src/Example.Database/Data/AppDbContext.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Example.Database.Domain; -using Microsoft.EntityFrameworkCore; - -namespace Example.Database.Data; - -public class AppDbContext : DbContext -{ - public AppDbContext(DbContextOptions options) : base(options) - { } - - public DbSet Persons { get; set; } = null!; -} \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Database/Data/Migrations/AppDbContextModelSnapshot.cs b/samples/UnitTests/src/Example.Database/Data/Migrations/AppDbContextModelSnapshot.cs deleted file mode 100644 index 2988579..0000000 --- a/samples/UnitTests/src/Example.Database/Data/Migrations/AppDbContextModelSnapshot.cs +++ /dev/null @@ -1,43 +0,0 @@ -// -using Example.Database.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Example.Database.Data.Migrations -{ - [DbContext(typeof(AppDbContext))] - partial class AppDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "6.0.6") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Example.Database.Domain.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Persons"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/samples/UnitTests/src/Example.Database/Data/Migrations/Initial.Designer.cs b/samples/UnitTests/src/Example.Database/Data/Migrations/Initial.Designer.cs deleted file mode 100644 index 309aa90..0000000 --- a/samples/UnitTests/src/Example.Database/Data/Migrations/Initial.Designer.cs +++ /dev/null @@ -1,45 +0,0 @@ -// -using Example.Database.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Example.Database.Data.Migrations -{ - [DbContext(typeof(AppDbContext))] - [Migration("20220628185129_Initial")] - partial class Initial - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "6.0.6") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Example.Database.Domain.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Persons"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/samples/UnitTests/src/Example.Database/Data/Migrations/Initial.cs b/samples/UnitTests/src/Example.Database/Data/Migrations/Initial.cs deleted file mode 100644 index 77f0350..0000000 --- a/samples/UnitTests/src/Example.Database/Data/Migrations/Initial.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Example.Database.Data.Migrations -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Persons", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Persons", x => x.Id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Persons"); - } - } -} diff --git a/samples/UnitTests/src/Example.Database/Domain/Person.cs b/samples/UnitTests/src/Example.Database/Domain/Person.cs deleted file mode 100644 index c01c785..0000000 --- a/samples/UnitTests/src/Example.Database/Domain/Person.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Example.Database.Domain; - -public class Person -{ - public Person(string name) => Name = name; - - public int Id { get; set; } - public string Name { get; set; } -} \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Database/Example.Database.csproj b/samples/UnitTests/src/Example.Database/Example.Database.csproj deleted file mode 100644 index 54b1a08..0000000 --- a/samples/UnitTests/src/Example.Database/Example.Database.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - diff --git a/samples/UnitTests/src/Example.Database/PersonService.cs b/samples/UnitTests/src/Example.Database/PersonService.cs deleted file mode 100644 index c362040..0000000 --- a/samples/UnitTests/src/Example.Database/PersonService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Example.Database.Data; -using Example.Database.Domain; -using Microsoft.EntityFrameworkCore; - -namespace Example.Database; - -public class PersonService -{ - private readonly AppDbContext dbContext; - public PersonService(AppDbContext dbContext) => this.dbContext = dbContext; - - public async Task> GetPersons() => await dbContext.Persons.ToListAsync(); -} \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Lib.Tests/AddSomethingHandlerTests.cs b/samples/UnitTests/src/Example.Lib.Tests/AddSomethingHandlerTests.cs deleted file mode 100644 index 4c5837f..0000000 --- a/samples/UnitTests/src/Example.Lib.Tests/AddSomethingHandlerTests.cs +++ /dev/null @@ -1,37 +0,0 @@ -using FluentAssertions; -using NSubstitute; - -namespace Example.Lib.Tests; - -public class AddSomethingHandlerTests : TestBase -{ - public AddSomethingHandlerTests(TestFixture fixture) : base(fixture) - { } - - [Fact] - public void Handle_AddsSomething() - { - // Arrange - // Set the test scoped ISomeService to always return number + 42 - GetInstance().Calculate(0).ReturnsForAnyArgs(ci => ci.Arg() + 42); - - // Act - // The AddSomethingHandler gets resolved using a fresh LifetimeScope - var result = Scoped(() => GetInstance().Handle(new AddSomething(3))); - - // Assert - result.Should().Be(45); - } - - [Fact] - public void Handle_AddsSomethingElse() - { - // Act - // No side effects from previous test as ISomeService was registered TestScoped - var result = Scoped(() => GetInstance().Handle(new AddSomething(3))); - - // Assert - // The mock for ISomeService.Calculate() returns the default value of int, 0, as it isn't configured otherwise. - result.Should().Be(0); - } -} \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Lib.Tests/Example.Lib.Tests.csproj b/samples/UnitTests/src/Example.Lib.Tests/Example.Lib.Tests.csproj deleted file mode 100644 index d60f8d1..0000000 --- a/samples/UnitTests/src/Example.Lib.Tests/Example.Lib.Tests.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - false - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/samples/UnitTests/src/Example.Lib.Tests/Properties/AssemblyInfo.cs b/samples/UnitTests/src/Example.Lib.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 2add585..0000000 --- a/samples/UnitTests/src/Example.Lib.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using Fusonic.Extensions.XUnit.Framework; - -[assembly:FusonicTestFramework] \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Lib.Tests/TestBase.cs b/samples/UnitTests/src/Example.Lib.Tests/TestBase.cs deleted file mode 100644 index 72adef4..0000000 --- a/samples/UnitTests/src/Example.Lib.Tests/TestBase.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Fusonic.Extensions.UnitTests; - -namespace Example.Lib.Tests; - -public class TestBase : TestBase -{ - public TestBase(TestFixture fixture) : base(fixture) - { } -} - -public class TestBase : UnitTest - where TFixture : TestFixture -{ - public TestBase(TFixture fixture) : base(fixture) - { } -} \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Lib.Tests/TestFixture.cs b/samples/UnitTests/src/Example.Lib.Tests/TestFixture.cs deleted file mode 100644 index 750ae4d..0000000 --- a/samples/UnitTests/src/Example.Lib.Tests/TestFixture.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Fusonic.Extensions.UnitTests; -using Fusonic.Extensions.UnitTests.SimpleInjector; -using NSubstitute; -using SimpleInjector; - -namespace Example.Lib.Tests; - -public class TestFixture : UnitTestFixture -{ - protected sealed override void RegisterCoreDependencies(Container container) - { - container.Register(); - container.RegisterTestScoped(() => Substitute.For()); - } -} \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Lib.Tests/Usings.cs b/samples/UnitTests/src/Example.Lib.Tests/Usings.cs deleted file mode 100644 index 8c927eb..0000000 --- a/samples/UnitTests/src/Example.Lib.Tests/Usings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Xunit; \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Lib/AddSomething.cs b/samples/UnitTests/src/Example.Lib/AddSomething.cs deleted file mode 100644 index 0f32035..0000000 --- a/samples/UnitTests/src/Example.Lib/AddSomething.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Example.Lib; - -public record AddSomething(int Number); \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Lib/AddSomethingHandler.cs b/samples/UnitTests/src/Example.Lib/AddSomethingHandler.cs deleted file mode 100644 index 97643cc..0000000 --- a/samples/UnitTests/src/Example.Lib/AddSomethingHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Example.Lib; - -public class AddSomethingHandler -{ - private readonly ISomeService someService; - - public AddSomethingHandler(ISomeService someService) - => this.someService = someService; - - public int Handle(AddSomething request) - => someService.Calculate(request.Number); -} \ No newline at end of file diff --git a/samples/UnitTests/src/Example.Lib/Example.Lib.csproj b/samples/UnitTests/src/Example.Lib/Example.Lib.csproj deleted file mode 100644 index be4388a..0000000 --- a/samples/UnitTests/src/Example.Lib/Example.Lib.csproj +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/samples/UnitTests/src/Example.Lib/ISomeService.cs b/samples/UnitTests/src/Example.Lib/ISomeService.cs deleted file mode 100644 index 5dce7fc..0000000 --- a/samples/UnitTests/src/Example.Lib/ISomeService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Example.Lib; - -public interface ISomeService -{ - int Calculate(int number); -} \ No newline at end of file diff --git a/samples/UnitTests/src/Example.sln b/samples/UnitTests/src/Example.sln deleted file mode 100644 index 5f40901..0000000 --- a/samples/UnitTests/src/Example.sln +++ /dev/null @@ -1,54 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.2.32602.215 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{161038E0-4983-469C-AF70-44F65D25F7BD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.Lib", "Example.Lib\Example.Lib.csproj", "{6A704D7E-7415-4691-A30A-A62B1CEBEEE1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6AE67147-534C-467D-BBE3-2B3FCC78A4FB}" - ProjectSection(SolutionItems) = preProject - Directory.Build.props = Directory.Build.props - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.Database", "Example.Database\Example.Database.csproj", "{F6981AF8-8AC9-4AD3-BCCD-553E21C5B237}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.Database.Tests", "Example.Database.Tests\Example.Database.Tests.csproj", "{77479370-142C-408F-886D-60BFD20648DD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.Lib.Tests", "Example.Lib.Tests\Example.Lib.Tests.csproj", "{2B07E28B-55DB-4DCC-8842-0CAC7F45F1E6}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6A704D7E-7415-4691-A30A-A62B1CEBEEE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6A704D7E-7415-4691-A30A-A62B1CEBEEE1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6A704D7E-7415-4691-A30A-A62B1CEBEEE1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6A704D7E-7415-4691-A30A-A62B1CEBEEE1}.Release|Any CPU.Build.0 = Release|Any CPU - {F6981AF8-8AC9-4AD3-BCCD-553E21C5B237}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F6981AF8-8AC9-4AD3-BCCD-553E21C5B237}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F6981AF8-8AC9-4AD3-BCCD-553E21C5B237}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F6981AF8-8AC9-4AD3-BCCD-553E21C5B237}.Release|Any CPU.Build.0 = Release|Any CPU - {77479370-142C-408F-886D-60BFD20648DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {77479370-142C-408F-886D-60BFD20648DD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {77479370-142C-408F-886D-60BFD20648DD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {77479370-142C-408F-886D-60BFD20648DD}.Release|Any CPU.Build.0 = Release|Any CPU - {2B07E28B-55DB-4DCC-8842-0CAC7F45F1E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2B07E28B-55DB-4DCC-8842-0CAC7F45F1E6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2B07E28B-55DB-4DCC-8842-0CAC7F45F1E6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2B07E28B-55DB-4DCC-8842-0CAC7F45F1E6}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {77479370-142C-408F-886D-60BFD20648DD} = {161038E0-4983-469C-AF70-44F65D25F7BD} - {2B07E28B-55DB-4DCC-8842-0CAC7F45F1E6} = {161038E0-4983-469C-AF70-44F65D25F7BD} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {A3EACE59-0DBC-4C84-A359-2EC6F8675018} - EndGlobalSection -EndGlobal diff --git a/src/EntityFrameworkCore.Abstractions/src/EntityNotFoundException.cs b/src/Common/src/Entities/EntityNotFoundException.cs similarity index 94% rename from src/EntityFrameworkCore.Abstractions/src/EntityNotFoundException.cs rename to src/Common/src/Entities/EntityNotFoundException.cs index 28f6bc2..40873d6 100644 --- a/src/EntityFrameworkCore.Abstractions/src/EntityNotFoundException.cs +++ b/src/Common/src/Entities/EntityNotFoundException.cs @@ -3,7 +3,7 @@ using System.Runtime.Serialization; -namespace Fusonic.Extensions.EntityFrameworkCore.Abstractions; +namespace Fusonic.Extensions.Common.Entities; [Serializable] public sealed class EntityNotFoundException : Exception diff --git a/src/EntityFrameworkCore.Abstractions/src/IEntity.cs b/src/Common/src/Entities/IEntity.cs similarity index 80% rename from src/EntityFrameworkCore.Abstractions/src/IEntity.cs rename to src/Common/src/Entities/IEntity.cs index fcd00ad..a572cc1 100644 --- a/src/EntityFrameworkCore.Abstractions/src/IEntity.cs +++ b/src/Common/src/Entities/IEntity.cs @@ -1,7 +1,7 @@ // Copyright (c) Fusonic GmbH. All rights reserved. // Licensed under the MIT License. See LICENSE file in the project root for license information. -namespace Fusonic.Extensions.EntityFrameworkCore.Abstractions; +namespace Fusonic.Extensions.Common.Entities; public interface IEntity { } diff --git a/src/Common/test/Reflection/PropertyUtilTests.cs b/src/Common/test/Reflection/PropertyUtilTests.cs index cbddca4..eb11f6a 100644 --- a/src/Common/test/Reflection/PropertyUtilTests.cs +++ b/src/Common/test/Reflection/PropertyUtilTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. +// Copyright (c) Fusonic GmbH. All rights reserved. // Licensed under the MIT License. See LICENSE file in the project root for license information. using FluentAssertions; @@ -57,6 +57,6 @@ public void GetType_InvalidExpression_ThrowsException() public class TestClass { public bool IsBusy { get; set; } - public string Value { get; set; } = null!; + public required string Value { get; set; } } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 1a3615c..ee59a2b 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - 7.0.0-beta.1 + 7.0.0 net7.0 diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 81b7ddb..711ad3c 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,38 +1,38 @@ - true false - - + - + - - - - - + + + + + + - + - + + @@ -43,5 +43,4 @@ - \ No newline at end of file diff --git a/src/Email/src/CssInliner.cs b/src/Email/src/CssInliner.cs index 14432d8..171f8fe 100644 --- a/src/Email/src/CssInliner.cs +++ b/src/Email/src/CssInliner.cs @@ -7,7 +7,7 @@ namespace Fusonic.Extensions.Email; public class CssInliner { - public static string EmailCssContent { get; private set; } = null!; + public static string? EmailCssContent { get; private set; } public CssInliner(EmailOptions options) { diff --git a/src/Email/test/AssemblyInfo.cs b/src/Email/test/AssemblyInfo.cs deleted file mode 100644 index 37d8be3..0000000 --- a/src/Email/test/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -[assembly: Fusonic.Extensions.XUnit.Framework.FusonicTestFramework] \ No newline at end of file diff --git a/src/Email/test/Email.Tests.csproj b/src/Email/test/Email.Tests.csproj index a4c8da8..2fd9eeb 100644 --- a/src/Email/test/Email.Tests.csproj +++ b/src/Email/test/Email.Tests.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/Email/test/Models/RenderTestEmailViewModel.cs b/src/Email/test/Models/RenderTestEmailViewModel.cs index e099efb..7d7aadf 100644 --- a/src/Email/test/Models/RenderTestEmailViewModel.cs +++ b/src/Email/test/Models/RenderTestEmailViewModel.cs @@ -1,4 +1,4 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. +// Copyright (c) Fusonic GmbH. All rights reserved. // Licensed under the MIT License. See LICENSE file in the project root for license information. namespace Fusonic.Extensions.Email.Tests.Models; @@ -6,5 +6,5 @@ namespace Fusonic.Extensions.Email.Tests.Models; [EmailView("Emails/RenderTest")] public class RenderTestEmailViewModel { - public string SomeField { get; set; } = null!; + public required string SomeField { get; set; } } diff --git a/src/Email/test/RazorEmailRenderingServiceTests.cs b/src/Email/test/RazorEmailRenderingServiceTests.cs index b279a33..eefface 100644 --- a/src/Email/test/RazorEmailRenderingServiceTests.cs +++ b/src/Email/test/RazorEmailRenderingServiceTests.cs @@ -57,7 +57,7 @@ public async Task ReturnsSubjectFromResource_OrCustomSubjectAsFallback() var (subject, _) = await emailRenderingService.RenderAsync(new CultureTestEmailViewModel(), culture, subjectKey: subjectKey); subject.Should().Be(subjectKey); - var localizer = Container.GetInstance>()(); + var localizer = GetInstance>()(); localizer.GetString(subjectKey).Returns(new LocalizedString(subjectKey, "My fancy subject localized")); (subject, _) = await emailRenderingService.RenderAsync(new CultureTestEmailViewModel(), culture, subjectKey: subjectKey); diff --git a/src/Email/test/TestBase.cs b/src/Email/test/TestBase.cs index 4d96029..0a6a7f8 100644 --- a/src/Email/test/TestBase.cs +++ b/src/Email/test/TestBase.cs @@ -1,7 +1,8 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. +// Copyright (c) Fusonic GmbH. All rights reserved. // Licensed under the MIT License. See LICENSE file in the project root for license information. using Fusonic.Extensions.UnitTests; +using Fusonic.Extensions.UnitTests.SimpleInjector; namespace Fusonic.Extensions.Email.Tests; @@ -11,8 +12,8 @@ protected TestBase(TestFixture fixture) : base(fixture) { } } -public abstract class TestBase : UnitTest - where TFixture : TestFixture +public abstract class TestBase : DependencyInjectionUnitTest + where TFixture : SimpleInjectorTestFixture { protected TestBase(TFixture fixture) : base(fixture) { } diff --git a/src/Email/test/TestFixture.cs b/src/Email/test/TestFixture.cs index b406252..53c9789 100644 --- a/src/Email/test/TestFixture.cs +++ b/src/Email/test/TestFixture.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.Reflection; -using Fusonic.Extensions.UnitTests; +using Fusonic.Extensions.UnitTests.SimpleInjector; using MediatR; using MediatR.Pipeline; using Microsoft.AspNetCore.Mvc.ApplicationParts; @@ -14,7 +14,7 @@ namespace Fusonic.Extensions.Email.Tests; -public class TestFixture : UnitTestFixture +public class TestFixture : SimpleInjectorTestFixture { protected sealed override void RegisterCoreDependencies(Container container) { diff --git a/src/EntityFrameworkCore.Abstractions/src/EntityFrameworkCore.Abstractions.csproj b/src/EntityFrameworkCore.Abstractions/src/EntityFrameworkCore.Abstractions.csproj deleted file mode 100644 index a727a33..0000000 --- a/src/EntityFrameworkCore.Abstractions/src/EntityFrameworkCore.Abstractions.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - Fusonic.Extensions.EntityFrameworkCore.Abstractions - Fusonic.Extensions.EntityFrameworkCore.Abstractions - https://github.com/fusonic/dotnet-extensions - true - snupkg - bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml - Abstractions to the EF Core extensions without referencing EF Core (Entities) - - - - - - \ No newline at end of file diff --git a/src/EntityFrameworkCore/src/EntityFrameworkCore.csproj b/src/EntityFrameworkCore/src/EntityFrameworkCore.csproj index b6c5ba2..0beb704 100644 --- a/src/EntityFrameworkCore/src/EntityFrameworkCore.csproj +++ b/src/EntityFrameworkCore/src/EntityFrameworkCore.csproj @@ -15,6 +15,5 @@ - diff --git a/src/EntityFrameworkCore/src/QueryableExtensions.cs b/src/EntityFrameworkCore/src/QueryableExtensions.cs index bfc330d..7bc975f 100644 --- a/src/EntityFrameworkCore/src/QueryableExtensions.cs +++ b/src/EntityFrameworkCore/src/QueryableExtensions.cs @@ -3,7 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; -using Fusonic.Extensions.EntityFrameworkCore.Abstractions; +using Fusonic.Extensions.Common.Entities; using Microsoft.EntityFrameworkCore; namespace Fusonic.Extensions.EntityFrameworkCore; @@ -28,6 +28,22 @@ public static Task IsRequiredAsync(this Task entity) where T : class, IEntity => entity.ContinueWith(t => t.Result.IsRequired(), TaskContinuationOptions.NotOnFaulted); + /// Determines if the entity with the given ID exists. Throws a EntityNotFoundException if it does not. + public static async Task IsRequiredAsync(this DbSet dbSet, TId id, CancellationToken cancellationToken = default) + where T : class, IEntity + where TId : struct + { + if (!await ExistsAsync(dbSet, id, cancellationToken)) + throw new EntityNotFoundException(typeof(T), id); + } + + /// Determines if the query returns any result (AnyAsync). Throws a EntityNotFoundException if it does not. + public static async Task IsRequiredAsync(this IQueryable query, CancellationToken cancellationToken = default) + { + if (!await query.AnyAsync(cancellationToken)) + throw new EntityNotFoundException(typeof(T)); + } + /// Check in the if the entity is available /// is a typeof an /// is a typeof an Id @@ -91,13 +107,4 @@ public static async Task SingleRequiredAsync(this IQueryable query public static async Task ExistsAsync(this DbSet dbSet, TId id, CancellationToken cancellationToken = default) where T : class, IEntity where TId : struct => await dbSet.AnyAsync(d => Equals(d.Id, id), cancellationToken); - - /// Determines if the entity with the given ID exists. Throws a EntityNotFoundException if it does not. - public static async Task RequireAsync(this DbSet dbSet, TId id, CancellationToken cancellationToken = default) - where T : class, IEntity - where TId : struct - { - if (!await ExistsAsync(dbSet, id, cancellationToken)) - throw new EntityNotFoundException(typeof(T), id); - } } \ No newline at end of file diff --git a/src/EntityFrameworkCore/test/Data/TestDbContext.cs b/src/EntityFrameworkCore/test/Data/TestDbContext.cs index 366a838..c96cafe 100644 --- a/src/EntityFrameworkCore/test/Data/TestDbContext.cs +++ b/src/EntityFrameworkCore/test/Data/TestDbContext.cs @@ -9,5 +9,5 @@ public class TestDbContext : DbContext { public TestDbContext(DbContextOptions options) : base(options) { } - public DbSet SampleDomainEntities { get; set; } = null!; + public DbSet SampleDomainEntities => Set(); } \ No newline at end of file diff --git a/src/EntityFrameworkCore/test/QueryableExtensionsTests.cs b/src/EntityFrameworkCore/test/QueryableExtensionsTests.cs index c8f2675..77b94d3 100644 --- a/src/EntityFrameworkCore/test/QueryableExtensionsTests.cs +++ b/src/EntityFrameworkCore/test/QueryableExtensionsTests.cs @@ -2,7 +2,7 @@ // Licensed under the MIT License. See LICENSE file in the project root for license information. using FluentAssertions; -using Fusonic.Extensions.EntityFrameworkCore.Abstractions; +using Fusonic.Extensions.Common.Entities; using Fusonic.Extensions.EntityFrameworkCore.Tests.Data; using Microsoft.EntityFrameworkCore; using Xunit; @@ -25,7 +25,7 @@ public QueryableExtensionsTests() } [Fact] - public async Task IsRequired_Succeeds() + public async Task IsRequired_Entity_Succeeds() { // Arrange var sampleEntity = await CreateSampleEntity(); @@ -38,7 +38,7 @@ public async Task IsRequired_Succeeds() } [Fact] - public void IsRequired_ThrowsException() + public void IsRequired_Entity_ThrowsException() { // Act var errorAction = () => testDbContext.SampleDomainEntities.SingleOrDefault(s => s.Id == Guid.NewGuid()).IsRequired(); @@ -50,7 +50,7 @@ public void IsRequired_ThrowsException() } [Fact] - public async Task IsRequiredAsync_Succeeds() + public async Task IsRequiredAsync_Task_Succeeds() { // Arrange var sampleEntity = await CreateSampleEntity(); @@ -63,7 +63,7 @@ public async Task IsRequiredAsync_Succeeds() } [Fact] - public async Task IsRequiredAsync_ThrowsException() + public async Task IsRequiredAsync_Task_ThrowsException() { // Act var errorAction = () => testDbContext.SampleDomainEntities.SingleOrDefaultAsync(s => s.Id == Guid.Empty).IsRequiredAsync(); @@ -74,6 +74,58 @@ await errorAction.Should() .WithMessage($"Could not find entity of type '{nameof(SampleDomainEntity)}'."); } + [Fact] + public async Task IsRequiredAsync_DbSet_Succeeds() + { + // Arrange + var sampleEntity = await CreateSampleEntity(); + + // Act + await testDbContext.SampleDomainEntities.IsRequiredAsync(sampleEntity.Id); + + // Assert, throws no exception + } + + [Fact] + public async Task IsRequiredAsync_DbSet_ThrowsException() + { + // Arrange + var guid = Guid.NewGuid(); + + // Act + var errorAction = () => testDbContext.SampleDomainEntities.IsRequiredAsync(guid); + + // Assert + (await errorAction.Should().ThrowAsync()) + .WithMessage($"Could not find entity of type '{nameof(SampleDomainEntity)}' with id {guid}."); + } + + [Fact] + public async Task IsRequiredAsync_Queryable_Succeeds() + { + // Arrange + var sampleEntity = await CreateSampleEntity(); + + // Act + await testDbContext.SampleDomainEntities.Where(e => e.Id == sampleEntity.Id).IsRequiredAsync(); + + // Assert, throws no exception + } + + [Fact] + public async Task IsRequiredAsync_Queryable_ThrowsException() + { + // Arrange + var guid = Guid.NewGuid(); + + // Act + var errorAction = () => testDbContext.SampleDomainEntities.Where(e => e.Id == guid).IsRequiredAsync(); + + // Assert + (await errorAction.Should().ThrowAsync()) + .WithMessage($"Could not find entity of type '{nameof(SampleDomainEntity)}'."); + } + [Fact] public async Task SingleRequiredAsync_Succeeds() { @@ -214,32 +266,6 @@ public async Task ExistsAsync_ReturnsFalse() result.Should().BeFalse(); } - [Fact] - public async Task RequireAsync_Succeeds() - { - // Arrange - var sampleEntity = await CreateSampleEntity(); - - // Act - await testDbContext.SampleDomainEntities.RequireAsync(sampleEntity.Id); - - // Assert, throws no exception - } - - [Fact] - public async Task RequireAsync_ThrowsException() - { - // Arrange - var guid = Guid.NewGuid(); - - // Act - var errorAction = () => testDbContext.SampleDomainEntities.RequireAsync(guid); - - // Assert - (await errorAction.Should().ThrowAsync()) - .WithMessage($"Could not find entity of type '{nameof(SampleDomainEntity)}' with id {guid}."); - } - private async Task CreateSampleEntity() { var guid = Guid.NewGuid(); diff --git a/src/EntityFrameworkCore/test/SampleDomainEntity.cs b/src/EntityFrameworkCore/test/SampleDomainEntity.cs index 165822a..1077789 100644 --- a/src/EntityFrameworkCore/test/SampleDomainEntity.cs +++ b/src/EntityFrameworkCore/test/SampleDomainEntity.cs @@ -1,7 +1,7 @@ // Copyright (c) Fusonic GmbH. All rights reserved. // Licensed under the MIT License. See LICENSE file in the project root for license information. -using Fusonic.Extensions.EntityFrameworkCore.Abstractions; +using Fusonic.Extensions.Common.Entities; namespace Fusonic.Extensions.EntityFrameworkCore.Tests; diff --git a/src/Hosting/test/Hosting.Tests.csproj b/src/Hosting/test/Hosting.Tests.csproj index ee2269d..aea9ebe 100644 --- a/src/Hosting/test/Hosting.Tests.csproj +++ b/src/Hosting/test/Hosting.Tests.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/Hosting/test/Properties/AssemblyInfo.cs b/src/Hosting/test/Properties/AssemblyInfo.cs deleted file mode 100644 index 37d8be3..0000000 --- a/src/Hosting/test/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -[assembly: Fusonic.Extensions.XUnit.Framework.FusonicTestFramework] \ No newline at end of file diff --git a/src/Hosting/test/TestBase.cs b/src/Hosting/test/TestBase.cs deleted file mode 100644 index e130184..0000000 --- a/src/Hosting/test/TestBase.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Fusonic.Extensions.UnitTests; - -namespace Fusonic.Extensions.Hosting.Tests; - -public class TestBase : UnitTest - where TFixture : TestFixture -{ - public TestBase(TFixture fixture) : base(fixture) - { } -} - -public class TestBase : TestBase -{ - public TestBase(TestFixture fixture) : base(fixture) - { } -} diff --git a/src/Hosting/test/TestFixture.cs b/src/Hosting/test/TestFixture.cs index f0ba3df..0f48eea 100644 --- a/src/Hosting/test/TestFixture.cs +++ b/src/Hosting/test/TestFixture.cs @@ -1,13 +1,13 @@ // Copyright (c) Fusonic GmbH. All rights reserved. // Licensed under the MIT License. See LICENSE file in the project root for license information. -using Fusonic.Extensions.UnitTests; +using Fusonic.Extensions.UnitTests.SimpleInjector; using Microsoft.Extensions.DependencyInjection; using SimpleInjector; namespace Fusonic.Extensions.Hosting.Tests; -public class TestFixture : UnitTestFixture +public class TestFixture : SimpleInjectorTestFixture { protected sealed override void RegisterCoreDependencies(Container container) { @@ -17,4 +17,4 @@ protected sealed override void RegisterCoreDependencies(Container container) _ = services.BuildServiceProvider().UseSimpleInjector(container); } -} +} \ No newline at end of file diff --git a/src/Hosting/test/TimedHostedService/TimedHostedServiceTests.cs b/src/Hosting/test/TimedHostedService/TimedHostedServiceTests.cs index e807a5c..06a0b63 100644 --- a/src/Hosting/test/TimedHostedService/TimedHostedServiceTests.cs +++ b/src/Hosting/test/TimedHostedService/TimedHostedServiceTests.cs @@ -4,17 +4,16 @@ using System.Net; using FluentAssertions; using Fusonic.Extensions.Hosting.TimedHostedService; -using Fusonic.Extensions.UnitTests.SimpleInjector; +using Fusonic.Extensions.UnitTests; using NSubstitute; using SimpleInjector; using Xunit; namespace Fusonic.Extensions.Hosting.Tests.TimedHostedService; -public class TimedHostedServiceTests : TestBase +public class TimedHostedServiceTests : DependencyInjectionUnitTest { - public TimedHostedServiceTests(TimedHostedServiceFixture fixture) : base(fixture) - { } + public TimedHostedServiceTests(TimedHostedServiceFixture fixture) : base(fixture) => GetInstance().Reset(); [Fact] public async Task Start_RunsTask_ImmediatelyAfterStart() @@ -35,7 +34,7 @@ public async Task Start_RunsTask_ImmediatelyAfterStart() public async Task RunTask_CrashesOnStartup_TimedHostedServiceDoesNotCare() { using var timedHostedService = GetInstance>(); - Func act = () => timedHostedService.StartAsync(CancellationToken.None); + var act = () => timedHostedService.StartAsync(CancellationToken.None); await act.Should().NotThrowAsync(); } @@ -121,6 +120,12 @@ private class Service public bool RunCalled { get; private set; } public CancellationToken CancellationToken { get; private set; } + public void Reset() + { + CancellationToken = CancellationToken.None; + RunCalled = false; + } + public Task Run(CancellationToken cancellationToken) { CancellationToken = cancellationToken; @@ -161,8 +166,8 @@ protected override void RegisterDependencies(Container container) AddTimedHostedService(_ => { }, (s, c) => s.Run(c)); AddTimedHostedService(_ => { }, (s, _) => s.Run()); - container.RegisterTestScoped(); - container.RegisterTestScoped(() => + container.Register(); + container.Register(() => { var httpClientFactory = Substitute.For(); var messageHandlerMock = container.GetInstance(); @@ -177,9 +182,9 @@ void AddTimedHostedService(Action configur configureTimedHostedService(settings); var hostSettings = new TimedHostedService.Settings(TimeSpan.FromSeconds(settings.Interval), settings.WatchdogUri, executeTask); - container.RegisterTestScoped>(); - container.RegisterTestScoped(); - container.RegisterTestScoped(() => hostSettings); + container.Register>(); + container.RegisterSingleton(); + container.Register(() => hostSettings); } } } diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/src/DatabaseHelper.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/src/DatabaseHelper.cs new file mode 100644 index 0000000..7571b32 --- /dev/null +++ b/src/UnitTests.EntityFrameworkCore.Npgsql/src/DatabaseHelper.cs @@ -0,0 +1,62 @@ +// Copyright (c) Fusonic GmbH. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for license information. + +using Npgsql; + +namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql; + +internal static class DatabaseHelper +{ + private static bool templateCreated; + private static Exception? creationFailedException; + private static readonly SemaphoreSlim CreationSync = new(1); + + /// + /// Checks if a DB template exists. If it does not, the template gets created using the given action. + /// Does not check if template is up to date if it does exist. + /// Once the template is created, all future calls to this method just return without checking the template again. + /// + /// Connection string to the template database. + /// Action to be executed to create the database. + /// When set to true, do not check if the template database exists, but always recreate the template on the first run. Defaults to false. + public static async Task EnsureCreated(string connectionString, Func createTemplate, bool alwaysCreateTemplate = false) + { + if (templateCreated) + return; + + await CreationSync.WaitAsync(); + try + { + if (templateCreated) + return; + + if (creationFailedException != null) + throw new InvalidOperationException("Template creation failed in another unit test. See InnerException for details.", creationFailedException); + + if (!alwaysCreateTemplate) + { + templateCreated = await PostgreSqlUtil.CheckDatabaseExists(connectionString); + if (templateCreated) + return; + } + + try + { + await createTemplate(connectionString); + } + catch (Exception e) + { + creationFailedException = e; + throw; + } + + NpgsqlConnection.ClearAllPools(); + + templateCreated = true; + } + finally + { + CreationSync.Release(); + } + } +} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/src/DbContextOptionsBuilderExtensions.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/src/DbContextOptionsBuilderExtensions.cs new file mode 100644 index 0000000..d3444c4 --- /dev/null +++ b/src/UnitTests.EntityFrameworkCore.Npgsql/src/DbContextOptionsBuilderExtensions.cs @@ -0,0 +1,14 @@ +// Copyright (c) Fusonic GmbH. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for license information. + +using Microsoft.EntityFrameworkCore; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; + +namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql; + +public static class DbContextOptionsBuilderExtensions +{ + public static DbContextOptionsBuilder UseNpgsqlDatabasePerTest(this DbContextOptionsBuilder builder, NpgsqlDatabasePerTestStore testStore, Action? npgsqlOptionsAction = null) + => builder.UseNpgsql(testStore.ConnectionString, npgsqlOptionsAction) + .AddInterceptors(new ConnectionOpeningInterceptor(testStore.CreateDatabase)); +} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/src/NpgsqlConnectionExtensions.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/src/NpgsqlConnectionExtensions.cs new file mode 100644 index 0000000..a60f810 --- /dev/null +++ b/src/UnitTests.EntityFrameworkCore.Npgsql/src/NpgsqlConnectionExtensions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Fusonic GmbH. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for license information. + +using Npgsql; + +namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql; + +internal static class NpgsqlConnectionExtensions +{ + public static async Task ExecuteAsync(this NpgsqlConnection connection, string sql) + { + await using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + await cmd.ExecuteNonQueryAsync(); + } +} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/src/NpgsqlDatabasePerTestStore.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/src/NpgsqlDatabasePerTestStore.cs new file mode 100644 index 0000000..d6eddc3 --- /dev/null +++ b/src/UnitTests.EntityFrameworkCore.Npgsql/src/NpgsqlDatabasePerTestStore.cs @@ -0,0 +1,80 @@ +// Copyright (c) Fusonic GmbH. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for license information. + +using Npgsql; +using Polly; + +namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql; + +public class NpgsqlDatabasePerTestStore : ITestStore +{ + private readonly NpgsqlDatabasePerTestStoreOptions options; + private readonly NpgsqlConnectionStringBuilder connectionStringBuilder; + + private readonly string templateDatabaseName; + private readonly string postgresConnectionString; + + public string ConnectionString => connectionStringBuilder.ConnectionString; + + private bool isDbCreated; + + public NpgsqlDatabasePerTestStore(NpgsqlDatabasePerTestStoreOptions options) + { + this.options = new NpgsqlDatabasePerTestStoreOptions(options); + + connectionStringBuilder = new NpgsqlConnectionStringBuilder(options.ConnectionString); + + templateDatabaseName = connectionStringBuilder.Database + ?? throw new ArgumentException("Missing template database in connection string."); + + connectionStringBuilder.Database = "postgres"; + postgresConnectionString = connectionStringBuilder.ConnectionString; + + OnTestConstruction(); + } + + public void OnTestConstruction() + { + connectionStringBuilder.Database = Convert.ToBase64String(Guid.NewGuid().ToByteArray()).TrimEnd('='); + isDbCreated = false; + } + + public async Task OnTestEnd() + { + if (!isDbCreated) + return; + + await using var connection = new NpgsqlConnection(postgresConnectionString); + await connection.OpenAsync(); + await connection.ExecuteAsync($@"DROP DATABASE IF EXISTS ""{connectionStringBuilder.Database}"" WITH (FORCE)"); + } + + public async Task CreateDatabase() + { + if (isDbCreated) + return; + + if (options.TemplateCreator != null) + await DatabaseHelper.EnsureCreated(options.ConnectionString!, options.TemplateCreator, options.AlwaysCreateTemplate); + + // Creating a DB from a template can cause an exception when done in parallel. + // The lock usually prevents this, however, we still encounter race conditions + // where we just have to retry. + // 55006: source database "test_template" is being accessed by other users + await Policy.Handle(e => e.SqlState == "55006") + .WaitAndRetryAsync(30, _ => TimeSpan.FromMilliseconds(500)) + .ExecuteAsync(CreateDb); + + async Task CreateDb() + { + if (isDbCreated) + return; + + await using var connection = new NpgsqlConnection(postgresConnectionString); + await connection.OpenAsync(); + await connection.ExecuteAsync($@"CREATE DATABASE ""{connectionStringBuilder.Database}"" TEMPLATE ""{templateDatabaseName}"""); + + isDbCreated = true; + } + } +} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/src/NpgsqlDatabasePerTestStoreOptions.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/src/NpgsqlDatabasePerTestStoreOptions.cs new file mode 100644 index 0000000..fb2b900 --- /dev/null +++ b/src/UnitTests.EntityFrameworkCore.Npgsql/src/NpgsqlDatabasePerTestStoreOptions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Fusonic GmbH. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for license information. + +namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql; + +public class NpgsqlDatabasePerTestStoreOptions +{ + public NpgsqlDatabasePerTestStoreOptions() + { } + + internal NpgsqlDatabasePerTestStoreOptions(NpgsqlDatabasePerTestStoreOptions copyFrom) + { + ConnectionString = copyFrom.ConnectionString; + TemplateCreator = copyFrom.TemplateCreator; + AlwaysCreateTemplate = copyFrom.AlwaysCreateTemplate; + } + + /// Connection string to the template database. + public string? ConnectionString { get; set; } + + /// Action to create the database template on first connect. Only gets executed once. If AlwaysCreateTemplate is set to false, the action only gets executed if the template database does not exist. + public Func? TemplateCreator { get; set; } + + /// Ignores an existing template database and always recreates the template on the first run. Ignored, if TemplateCreator is null. + public bool AlwaysCreateTemplate { get; set; } +} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/src/NpgsqlTestDatabaseProvider.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/src/NpgsqlTestDatabaseProvider.cs deleted file mode 100644 index fd12a4c..0000000 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/src/NpgsqlTestDatabaseProvider.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Dapper; -using Microsoft.EntityFrameworkCore; -using Npgsql; -using Polly; - -namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql; - -public class NpgsqlTestDatabaseProvider : ITestDatabaseProvider -{ - private readonly NpgsqlTestDatabaseProviderSettings settings; - private readonly string templateName; - - private bool dbCreated; - - /// - public string TestDbName { get; } = Convert.ToBase64String(Guid.NewGuid().ToByteArray()).TrimEnd('='); - - public NpgsqlTestDatabaseProvider(NpgsqlTestDatabaseProviderSettings settings) - { - this.settings = settings; - - if (string.IsNullOrWhiteSpace(settings.TemplateConnectionString)) - throw new ArgumentException($"The {nameof(NpgsqlTestDatabaseProviderSettings)}.{nameof(NpgsqlTestDatabaseProviderSettings.TemplateConnectionString)} is empty. This is a required setting."); - - templateName = PostgreSqlUtil.GetDatabaseName(settings.TemplateConnectionString) - ?? throw new ArgumentException($"Could not find database in the {nameof(NpgsqlTestDatabaseProviderSettings.TemplateConnectionString)}."); - } - - /// - void ITestDatabaseProvider.CreateDatabase(DbContext dbContext) => CreateDatabase(); - - /// - public void CreateDatabase() - { - if (dbCreated) - return; - - // Creating a DB from a template can cause an exception when done in parallel. - // The lock usually prevents this, however, we still encounter race conditions - // where we just have to retry. - // 55006: source database "test_template" is being accessed by other users - Policy.Handle(e => e.SqlState == "55006") - .WaitAndRetry(30, _ => TimeSpan.FromMilliseconds(500)) - .Execute(CreateDb); - - void CreateDb() - { - if (dbCreated) - return; - - using var connection = PostgreSqlUtil.CreatePostgresDbConnection(settings.TemplateConnectionString); - connection.Execute($@"CREATE DATABASE ""{TestDbName}"" TEMPLATE ""{templateName}"""); - - dbCreated = true; - } - } - - /// - void ITestDatabaseProvider.DropDatabase(DbContext dbContext) => DropDatabase(); - - /// - public void DropDatabase() - { - if (!dbCreated) - return; - - PostgreSqlUtil.DropDb(settings.TemplateConnectionString, TestDbName); - } -} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/src/NpgsqlTestDatabaseProviderSettings.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/src/NpgsqlTestDatabaseProviderSettings.cs deleted file mode 100644 index ee51957..0000000 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/src/NpgsqlTestDatabaseProviderSettings.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql; - -public class NpgsqlTestDatabaseProviderSettings -{ - public string TemplateConnectionString { get; init; } = null!; -} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/src/PostgreSqlUtil.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/src/PostgreSqlUtil.cs index 7359ba0..32a7071 100644 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/src/PostgreSqlUtil.cs +++ b/src/UnitTests.EntityFrameworkCore.Npgsql/src/PostgreSqlUtil.cs @@ -3,37 +3,24 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Text.RegularExpressions; -using Dapper; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; using Npgsql; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using NpgsqlTypes; namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql; -public static partial class PostgreSqlUtil +public static class PostgreSqlUtil { - private static readonly Regex DatabaseRegex = GetDatabaseRegex(); - - // Returns the database name in the connection string or null, if it could not be matched. - public static string? GetDatabaseName(string connectionString) - { - var match = DatabaseRegex.Match(connectionString); - if (!match.Success || match.Groups.Count != 2) - return null; - - return match.Groups[1].Value.Trim(); - } - /// Creates a test database. /// Connection string to the test database. The database does not have to exist. /// Returns a DbContext using the given options. /// The configuration action for .UseNpgsql(). /// Optional seed action that gets executed after creating the database. /// Logger. Defaults to console logger. - public static void CreateTestDbTemplate( + public static async Task CreateTestDbTemplate( string connectionString, Func, TDbContext> dbContextFactory, Action? npgsqlOptionsAction = null, @@ -43,64 +30,80 @@ public static void CreateTestDbTemplate( { logger ??= CreateConsoleLogger(); - // Open connection to the postgres-DB (for drop, create, alter) - using var connection = CreatePostgresDbConnection(connectionString); - // Get database from connection string. - var dbName = GetDatabaseName(connectionString); + var csBuilder = new NpgsqlConnectionStringBuilder(connectionString); + var dbName = csBuilder.Database; AssertNotPostgres(dbName); - // Drop existing Test-DB - if (connection.ExecuteScalar("SELECT EXISTS(SELECT * FROM pg_catalog.pg_database WHERE datname=@dbName)", new { dbName })) + // Open connection to the postgres-DB (for drop, create, alter) + csBuilder.Database = "postgres"; + await using (var connection = new NpgsqlConnection(csBuilder.ConnectionString)) { - logger.LogInformation("Dropping database {Database}", dbName); - DropDb(connectionString, dbName); - } + await connection.OpenAsync(); - // Create database - logger.LogInformation("Creating database {Database}", dbName); - connection.Execute($@"CREATE DATABASE ""{dbName}"" TEMPLATE template0 IS_TEMPLATE true"); - - // Migrate & run seed - var options = new DbContextOptionsBuilder() - .UseNpgsql(connectionString, npgsqlOptionsAction) - .LogTo( - (eventId, _) => eventId != RelationalEventId.CommandExecuted, - eventData => logger.Log(eventData.LogLevel, eventData.EventId, "[EF] {Message}", eventData.ToString())) - .Options; - using (var dbContext = dbContextFactory(options)) - { - logger.LogInformation("Running migrations"); - dbContext.Database.Migrate(); + // Drop existing Test-DB + if (await CheckDatabaseExists(connection, dbName)) + { + logger.LogInformation("Dropping database {Database}", dbName); + + await connection.ExecuteAsync($@"ALTER DATABASE ""{dbName}"" IS_TEMPLATE false"); + await connection.ExecuteAsync($@"DROP DATABASE ""{dbName}"" WITH (FORCE)"); + } - if (seed != null) + // Create database + logger.LogInformation("Creating database {Database}", dbName); + await connection.ExecuteAsync($@"CREATE DATABASE ""{dbName}"" TEMPLATE template0 IS_TEMPLATE true"); + + // Migrate & run seed + var options = new DbContextOptionsBuilder() + .UseNpgsql(connectionString, npgsqlOptionsAction) + .LogTo( + (eventId, _) => eventId != RelationalEventId.CommandExecuted, + eventData => logger.Log(eventData.LogLevel, eventData.EventId, "[EF] {Message}", eventData.ToString())) + .Options; + + await using (var dbContext = dbContextFactory(options)) { - logger.LogInformation("Running seed"); - seed(dbContext).Wait(); + logger.LogInformation("Running migrations"); + await dbContext.Database.MigrateAsync(); + + if (seed != null) + { + logger.LogInformation("Running seed"); + await seed(dbContext); + } } - } - //Convert to template - logger.LogInformation("Setting connection limit on template"); - connection.Execute($@"ALTER DATABASE ""{dbName}"" CONNECTION LIMIT 0"); + //Convert to template + logger.LogInformation("Setting connection limit on template"); + await connection.ExecuteAsync($@"ALTER DATABASE ""{dbName}"" CONNECTION LIMIT 0"); + } + NpgsqlConnection.ClearAllPools(); logger.LogInformation("Done"); } - /// Drops the given database. - /// Connection string to the postgres database. - /// Database that should be dropped. - public static void DropDb(string connectionString, string dbName) + public static async Task CheckDatabaseExists(string connectionString) { - AssertNotPostgres(dbName); + var csBuilder = new NpgsqlConnectionStringBuilder(connectionString); + var dbName = csBuilder.Database!; + + csBuilder.Database = "postgres"; + await using var connection = new NpgsqlConnection(csBuilder.ConnectionString); + connection.Open(); + var exists = await CheckDatabaseExists(connection, dbName); - using var connection = CreatePostgresDbConnection(connectionString); - var exists = connection.ExecuteScalar("SELECT EXISTS(SELECT * FROM pg_database WHERE datname=@dbName)", new { dbName }); - if (!exists) - return; + return exists; + } + + private static async Task CheckDatabaseExists(NpgsqlConnection connection, string dbName) + { + var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT EXISTS(SELECT * FROM pg_database WHERE datname=@dbName)"; + cmd.Parameters.Add("@dbName", NpgsqlDbType.Varchar).Value = dbName; - connection.Execute($@"ALTER DATABASE ""{dbName}"" IS_TEMPLATE false"); - connection.Execute($@"DROP DATABASE ""{dbName}"" WITH (FORCE)"); + var result = await cmd.ExecuteScalarAsync(); + return result != null && (bool)result; } private static void AssertNotPostgres([NotNull] string? dbName) @@ -112,18 +115,7 @@ private static void AssertNotPostgres([NotNull] string? dbName) throw new ArgumentException("You can't do this on the postgres database."); } - /// Creates a connection using the given connection string, but replacing the database with postgres. - internal static NpgsqlConnection CreatePostgresDbConnection(string connectionString) - => new(ReplaceDatabaseName(connectionString, "postgres")); - - /// Replaces the database in a connection string with another one. - internal static string ReplaceDatabaseName(string connectionString, string dbName) - => DatabaseRegex.Replace(connectionString, $"Database={dbName}"); - private static ILogger CreateConsoleLogger() => LoggerFactory.Create(b => b.AddSimpleConsole(c => c.SingleLine = true)) .CreateLogger(nameof(PostgreSqlUtil)); - - [GeneratedRegex("Database=([^;]+)")] - private static partial Regex GetDatabaseRegex(); } \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/src/SimpleInjectorExtensions.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/src/SimpleInjectorExtensions.cs deleted file mode 100644 index f1099a9..0000000 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/src/SimpleInjectorExtensions.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Microsoft.EntityFrameworkCore; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using SimpleInjector; - -namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql; - -public static class SimpleInjectorExtensions -{ - /// - /// Registers a DbContext where each test gets an own database. Note: You need to additionally register an to support your specific database. - /// - /// The type of the EF Core DbContext to register - /// The simple injector container. - /// Configure the settings for the provider, like the connection string to the template. - /// If enabled, the dbContext logs to the test output. Can be helpful for debugging. Disabled by default. - /// An optional action to allow additional Npgsql-configuration. - /// An optional action to allow additional DbContextOptionsBuilder-configuration. - public static void RegisterNpgsqlDbContext( - this Container container, - Action configureSettings, - Action? npgsqlOptions = null, - Action>? dbContextBuilderOptions = null, - bool enableDbContextLogging = false) - where TDbContext : DbContext - => container.RegisterNpgsqlDbContext( - configureSettings, - npgsqlOptions, - dbContextBuilderOptions, - enableDbContextLogging); - - /// - /// Registers a DbContext where each test gets an own database. Note: You need to additionally register an to support your specific database. - /// - /// The type of the EF Core DbContext to register - /// The type of the test database provider - /// The type of the settings for the test database provider - /// The simple injector container. - /// Configure the settings for the provider, like the connection string to the template. - /// If enabled, the dbContext logs to the test output. Can be helpful for debugging. Disabled by default. - /// An optional action to allow additional Npgsql-configuration. - /// An optional action to allow additional DbContextOptionsBuilder-configuration. - public static void RegisterNpgsqlDbContext( - this Container container, - Action configureSettings, - Action? npgsqlOptions = null, - Action>? dbContextBuilderOptions = null, - bool enableDbContextLogging = false) - where TDbContext : DbContext - where TProvider : NpgsqlTestDatabaseProvider - where TSettings : NpgsqlTestDatabaseProviderSettings, new() - { - var settings = new TSettings(); - configureSettings(settings); - - container.RegisterInstance(settings); - - container.RegisterDbContext( - (dbName, builder) => - { - var connectionString = PostgreSqlUtil.ReplaceDatabaseName(settings.TemplateConnectionString, dbName); - builder.UseNpgsql(connectionString, npgsqlOptions); - dbContextBuilderOptions?.Invoke(builder); - }, enableDbContextLogging); - } -} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/src/UnitTests.EntityFrameworkCore.Npgsql.csproj b/src/UnitTests.EntityFrameworkCore.Npgsql/src/UnitTests.EntityFrameworkCore.Npgsql.csproj index 52d80c6..7d10f23 100644 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/src/UnitTests.EntityFrameworkCore.Npgsql.csproj +++ b/src/UnitTests.EntityFrameworkCore.Npgsql/src/UnitTests.EntityFrameworkCore.Npgsql.csproj @@ -17,9 +17,8 @@ - - + \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/template/Program.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/template/Program.cs deleted file mode 100644 index 8fd62b3..0000000 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/template/Program.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql; -using Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.Tests; - -// TODO: Remove with .NET 7.0.1, false positive fixed in https://github.com/dotnet/roslyn-analyzers/pull/6278 -#pragma warning disable CA1852 - -// Example: -// dotnet run --project ./src/UnitTests.EntityFrameworkCore.Npgsql/template/UnitTests.EntityFrameworkCore.Npgsql.TestDbTemplateCreator.csproj "Host=127.0.0.1;Port=5433;Database=fusonic_extensions_test;Username=postgres;Password=developer" - -if (args.Length == 0) -{ - Console.Out.WriteLine("Missing connection string."); - return 1; -} - -var connectionString = args[0]; - -PostgreSqlUtil.CreateTestDbTemplate(connectionString, o => new TestDbContext(o)); - -return 0; diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/template/UnitTests.EntityFrameworkCore.Npgsql.TestDbTemplateCreator.csproj b/src/UnitTests.EntityFrameworkCore.Npgsql/template/UnitTests.EntityFrameworkCore.Npgsql.TestDbTemplateCreator.csproj deleted file mode 100644 index ead7ffe..0000000 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/template/UnitTests.EntityFrameworkCore.Npgsql.TestDbTemplateCreator.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - false - false - Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.TestDbTemplateCreator - Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.TestDbTemplateCreator - - - - - - - diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/test/Migrations/.editorconfig b/src/UnitTests.EntityFrameworkCore.Npgsql/test/Migrations/.editorconfig deleted file mode 100644 index d49f9e4..0000000 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/test/Migrations/.editorconfig +++ /dev/null @@ -1,5 +0,0 @@ -# This .editorconfig disables the analyzers. This can be used for folders containing generated code, like the EF migrations directory. - -[*.cs] - -dotnet_analyzer_diagnostic.category-Style.severity = silent \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/test/Migrations/20221123105330_CreateTestDatabase.Designer.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/test/Migrations/20221123105330_CreateTestDatabase.Designer.cs deleted file mode 100644 index 0c3d9e1..0000000 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/test/Migrations/20221123105330_CreateTestDatabase.Designer.cs +++ /dev/null @@ -1,45 +0,0 @@ -// -using Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.Tests; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.Tests.Migrations -{ - [DbContext(typeof(TestDbContext))] - [Migration("20221123105330_CreateTestDatabase")] - partial class CreateTestDatabase - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "7.0.0") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.Tests.TestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Name") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("TestEntities"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/test/Migrations/20221123105330_CreateTestDatabase.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/test/Migrations/20221123105330_CreateTestDatabase.cs deleted file mode 100644 index 3092b6d..0000000 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/test/Migrations/20221123105330_CreateTestDatabase.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.Tests.Migrations -{ - /// - public partial class CreateTestDatabase : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "TestEntities", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_TestEntities", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "TestEntities"); - } - } -} diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/test/Migrations/TestDbContextModelSnapshot.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/test/Migrations/TestDbContextModelSnapshot.cs deleted file mode 100644 index 7dbbca1..0000000 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/test/Migrations/TestDbContextModelSnapshot.cs +++ /dev/null @@ -1,42 +0,0 @@ -// -using Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.Tests; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.Tests.Migrations -{ - [DbContext(typeof(TestDbContext))] - partial class TestDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "7.0.0") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.Tests.TestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Name") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("TestEntities"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/test/NpgsqlTestDatabaseProviderTests.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/test/NpgsqlTestDatabaseProviderTests.cs index 445c6f0..4543154 100644 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/test/NpgsqlTestDatabaseProviderTests.cs +++ b/src/UnitTests.EntityFrameworkCore.Npgsql/test/NpgsqlTestDatabaseProviderTests.cs @@ -6,11 +6,12 @@ using Npgsql; namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.Tests; -public class NpgsqlTestDatabaseProviderTests : TestBase + +public class NpgsqlDatabasePerTestStoreTests : TestBase { private static readonly List UsedDbNames = new(); - public NpgsqlTestDatabaseProviderTests(TestFixture fixture) : base(fixture) + public NpgsqlDatabasePerTestStoreTests(TestFixture fixture) : base(fixture) { } [Theory] @@ -24,7 +25,7 @@ public async Task EachTestGetsAnOwnDatabase_TestsDoNotAffectEachOther(int entity { ctx.AddRange(Enumerable.Range(1, entityCount).Select(i => new TestEntity { Name = "Name " + i })); await ctx.SaveChangesAsync(); - return PostgreSqlUtil.GetDatabaseName(ctx.Database.GetConnectionString()!)!; + return new NpgsqlConnectionStringBuilder(ctx.Database.GetConnectionString()).Database!; }); // Assert unique db names (not nice) @@ -40,63 +41,65 @@ public async Task EachTestGetsAnOwnDatabase_TestsDoNotAffectEachOther(int entity public async Task DropDatabase_DatabaseExists_DropsDatabase() { // Arrange - var provider = GetInstance(); - provider.CreateDatabase(); + var store = GetStore(); + await store.CreateDatabase(); // Act - provider.DropDatabase(); + await store.OnTestEnd(); // Assert var dbNames = await GetDatabases(); - dbNames.Should().NotContain(provider.TestDbName); + dbNames.Should().NotContain(store.ConnectionString); } [Fact] public async Task DropDatabase_DatabaseDoesNotExist_DoesNotThrowException() { // Arrange - var provider = GetInstance(); + var store = GetStore(); // Act - provider.DropDatabase(); + await store.OnTestEnd(); // Assert var dbNames = await GetDatabases(); - dbNames.Should().NotContain(provider.TestDbName); + dbNames.Should().NotContain(GetDbName(store)); } [Fact] public async Task CreateDatabase_CreatesTestDb() { // Arrange - var provider = GetInstance(); + var store = GetStore(); // Act - provider.CreateDatabase(); + await store.CreateDatabase(); // Assert var dbNames = await GetDatabases(); - dbNames.Should().Contain(provider.TestDbName); + dbNames.Should().Contain(GetDbName(store)); } [Fact] public void TestDbName_IsDifferentThanTemplate() { - var provider = GetInstance(); - var settings = GetInstance(); - var dbName = new NpgsqlConnectionStringBuilder(settings.TemplateConnectionString).Database; - - provider.TestDbName.Should().NotBe(dbName); + var store = GetStore(); + var settings = GetInstance(); + GetDbName(settings.ConnectionString!).Should().NotBe(GetDbName(store)); } private async Task> GetDatabases() { - var settings = GetInstance(); - var builder = new NpgsqlConnectionStringBuilder(settings.TemplateConnectionString); + var settings = GetInstance(); + var builder = new NpgsqlConnectionStringBuilder(settings.ConnectionString); builder.Database = "postgres"; await using var connection = new NpgsqlConnection(builder.ConnectionString); var dbNames = (await connection.QueryAsync("SELECT datname FROM pg_database")).ToList(); return dbNames; } -} + + private NpgsqlDatabasePerTestStore GetStore() => (NpgsqlDatabasePerTestStore)GetInstance(); + private static string GetDbName(NpgsqlDatabasePerTestStore store) => GetDbName(store.ConnectionString); + private static string GetDbName(string connectionString) => new NpgsqlConnectionStringBuilder(connectionString).Database!; +} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/test/Properties/AssemblyInfo.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/test/Properties/AssemblyInfo.cs deleted file mode 100644 index 37d8be3..0000000 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/test/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -[assembly: Fusonic.Extensions.XUnit.Framework.FusonicTestFramework] \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/test/TestBase.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/test/TestBase.cs index 2d0290e..067bc2c 100644 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/test/TestBase.cs +++ b/src/UnitTests.EntityFrameworkCore.Npgsql/test/TestBase.cs @@ -14,6 +14,4 @@ public abstract class TestBase : DatabaseUnitTest GetInstance().DropDatabase(); } diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/test/TestDbContext.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/test/TestDbContext.cs index 98bce05..a3f9753 100644 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/test/TestDbContext.cs +++ b/src/UnitTests.EntityFrameworkCore.Npgsql/test/TestDbContext.cs @@ -10,5 +10,5 @@ public class TestDbContext : DbContext public TestDbContext(DbContextOptions options) : base(options) { } - public DbSet TestEntities { get; set; } = null!; + public DbSet TestEntities => Set(); } \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/test/TestDbContextDesignTimeFactory.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/test/TestDbContextDesignTimeFactory.cs deleted file mode 100644 index 418438e..0000000 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/test/TestDbContextDesignTimeFactory.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; - -namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.Tests; - -public class TestDbContextDesignTimeFactory : IDesignTimeDbContextFactory -{ - public TestDbContext CreateDbContext(string[] args) => new(new DbContextOptionsBuilder().UseNpgsql("Host=localhost;Database=fusonic_extensions;Username=postgres;Password=developer").Options); -} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/test/TestFixture.cs b/src/UnitTests.EntityFrameworkCore.Npgsql/test/TestFixture.cs index 7ae0ec8..5caa566 100644 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/test/TestFixture.cs +++ b/src/UnitTests.EntityFrameworkCore.Npgsql/test/TestFixture.cs @@ -1,12 +1,30 @@ // Copyright (c) Fusonic GmbH. All rights reserved. // Licensed under the MIT License. See LICENSE file in the project root for license information. +using Fusonic.Extensions.UnitTests.ServiceProvider; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; -using SimpleInjector; +using Microsoft.Extensions.DependencyInjection; namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.Tests; -public class TestFixture : UnitTestFixture +public class TestFixture : ServiceProviderTestFixture { - protected sealed override void RegisterCoreDependencies(Container container) => container.RegisterNpgsqlDbContext(Configuration.Bind); + protected override void RegisterCoreDependencies(IServiceCollection services) + { + var testStoreOptions = Configuration.Get()!; + testStoreOptions.TemplateCreator = CreateDatabase; + + var testStore = new NpgsqlDatabasePerTestStore(testStoreOptions); + services.AddSingleton(testStore); + services.AddSingleton(testStoreOptions); + + services.AddDbContext(b => b.UseNpgsqlDatabasePerTest(testStore)); + } + + private static async Task CreateDatabase(string connectionString) + { + await using var dbContext = new TestDbContext(new DbContextOptionsBuilder().UseNpgsql(connectionString).Options); + await dbContext.Database.EnsureCreatedAsync(); + } } \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/test/UnitTests.EntityFrameworkCore.Npgsql.Tests.csproj b/src/UnitTests.EntityFrameworkCore.Npgsql/test/UnitTests.EntityFrameworkCore.Npgsql.Tests.csproj index e550344..cdbbf9b 100644 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/test/UnitTests.EntityFrameworkCore.Npgsql.Tests.csproj +++ b/src/UnitTests.EntityFrameworkCore.Npgsql/test/UnitTests.EntityFrameworkCore.Npgsql.Tests.csproj @@ -7,6 +7,7 @@ + all @@ -25,6 +26,7 @@ + diff --git a/src/UnitTests.EntityFrameworkCore.Npgsql/test/testsettings.json b/src/UnitTests.EntityFrameworkCore.Npgsql/test/testsettings.json index b1e05e0..9939447 100644 --- a/src/UnitTests.EntityFrameworkCore.Npgsql/test/testsettings.json +++ b/src/UnitTests.EntityFrameworkCore.Npgsql/test/testsettings.json @@ -1,3 +1,3 @@ { - "TemplateConnectionString": "Host=127.0.0.1;Port=5433;Database=fusonic_extensions_test;Username=postgres;Password=developer", + "ConnectionString": "Host=127.0.0.1;Port=5433;Database=fusonic_extensions_test;Username=postgres;Password=developer" } diff --git a/src/UnitTests.EntityFrameworkCore/src/ConnectionOpeningInterceptor.cs b/src/UnitTests.EntityFrameworkCore/src/ConnectionOpeningInterceptor.cs new file mode 100644 index 0000000..02c3a7d --- /dev/null +++ b/src/UnitTests.EntityFrameworkCore/src/ConnectionOpeningInterceptor.cs @@ -0,0 +1,31 @@ +// Copyright (c) Fusonic GmbH. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for license information. + +using System.Data.Common; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore; + +/// +/// This interceptor intercepts the connection opening event and executes the action defined in the ctor. +/// +public sealed class ConnectionOpeningInterceptor : DbConnectionInterceptor +{ + private readonly Func onOpening; + + public ConnectionOpeningInterceptor(Func onOpening) => this.onOpening = onOpening; + + public override InterceptionResult ConnectionOpening(DbConnection connection, ConnectionEventData eventData, InterceptionResult result) + { + // Need to force onOpening to run outside of XUnits synchronization context to avoid deadlocks (XUnit thread pool may be exhausted already) + Task.Run(onOpening).Wait(); + return base.ConnectionOpening(connection, eventData, result); + } + + public override async ValueTask ConnectionOpeningAsync(DbConnection connection, ConnectionEventData eventData, InterceptionResult result, CancellationToken cancellationToken = new()) + { + // Need to force onOpening to run outside of XUnits synchronization context to avoid deadlocks (XUnit thread pool may be exhausted already) + await onOpening().ConfigureAwait(false); + return await base.ConnectionOpeningAsync(connection, eventData, result, cancellationToken); + } +} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore/src/CreateDatabaseInterceptor.cs b/src/UnitTests.EntityFrameworkCore/src/CreateDatabaseInterceptor.cs deleted file mode 100644 index e46f36d..0000000 --- a/src/UnitTests.EntityFrameworkCore/src/CreateDatabaseInterceptor.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using System.Data.Common; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; - -namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore; - -/// -/// This interceptor creates a test database on first connection. This also works on pooled connections as the test databases -/// are unique per test and thus always require a new connection. -/// -internal sealed class CreateDatabaseInterceptor : DbConnectionInterceptor - where TDbContext : DbContext -{ - private readonly Action createDb; - private bool dbCreated; - private bool dbCreating; - - public CreateDatabaseInterceptor(Action createDb) => this.createDb = createDb; - - public override InterceptionResult ConnectionOpening(DbConnection connection, ConnectionEventData eventData, InterceptionResult result) - { - CreateDatabase(eventData.Context); - return base.ConnectionOpening(connection, eventData, result); - } - - public override async ValueTask ConnectionOpeningAsync(DbConnection connection, ConnectionEventData eventData, InterceptionResult result, CancellationToken cancellationToken = new()) - { - CreateDatabase(eventData.Context); - return await base.ConnectionOpeningAsync(connection, eventData, result, cancellationToken); - } - - private void CreateDatabase(DbContext? dbContext) - { - if (dbCreating || dbCreated || dbContext == null) - return; - - // Avoid stack overflow in scenarios where createDb() calls eg. dbContext.Database.EnsureCreated - dbCreating = true; - try - { - createDb((TDbContext)dbContext); - dbCreated = true; - } - catch - { - dbCreating = false; - } - } -} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore/src/DatabaseUnitTest`1.cs b/src/UnitTests.EntityFrameworkCore/src/DatabaseUnitTest`1.cs index 63193d6..0e5d272 100644 --- a/src/UnitTests.EntityFrameworkCore/src/DatabaseUnitTest`1.cs +++ b/src/UnitTests.EntityFrameworkCore/src/DatabaseUnitTest`1.cs @@ -3,13 +3,14 @@ using System.Diagnostics; using Microsoft.EntityFrameworkCore; +using Xunit; namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore; -public abstract class DatabaseUnitTest : UnitTest - where TFixture : UnitTestFixture +public abstract class DatabaseUnitTest : DependencyInjectionUnitTest, IAsyncLifetime + where TFixture : class, IDependencyInjectionTestFixture { - protected DatabaseUnitTest(TFixture fixture) : base(fixture) { } + protected DatabaseUnitTest(TFixture fixture) : base(fixture) => GetInstance().OnTestConstruction(); /// Executes a query in an own scope. [DebuggerStepThrough] @@ -60,12 +61,6 @@ protected Task QueryAsync(Func Task.CompletedTask; + public virtual async Task DisposeAsync() => await GetInstance().OnTestEnd(); } \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore/src/DatabaseUnitTest`2.cs b/src/UnitTests.EntityFrameworkCore/src/DatabaseUnitTest`2.cs index cebf458..6eb9026 100644 --- a/src/UnitTests.EntityFrameworkCore/src/DatabaseUnitTest`2.cs +++ b/src/UnitTests.EntityFrameworkCore/src/DatabaseUnitTest`2.cs @@ -8,7 +8,7 @@ namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore; public abstract class DatabaseUnitTest : DatabaseUnitTest where TDbContext : DbContext - where TFixture : UnitTestFixture + where TFixture : class, IDependencyInjectionTestFixture { protected DatabaseUnitTest(TFixture fixture) : base(fixture) { } diff --git a/src/UnitTests.EntityFrameworkCore/src/ITestDatabaseProvider.cs b/src/UnitTests.EntityFrameworkCore/src/ITestDatabaseProvider.cs deleted file mode 100644 index 8c6e22a..0000000 --- a/src/UnitTests.EntityFrameworkCore/src/ITestDatabaseProvider.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Microsoft.EntityFrameworkCore; - -namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore; - -public interface ITestDatabaseProvider -{ - /// - /// Gets the name of the test database for one test. Can be different for each test. - /// - string TestDbName { get; } - - /// - /// Create the database for the currently running test. - /// - /// The DbContext with the connection pointing to the test database. The test database might not exist yet at this point. - void CreateDatabase(DbContext dbContext); - - /// - /// Drops the previously created test database. Can be called even if no database was created before. - /// - void DropDatabase(DbContext dbContext); -} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore/src/ITestStore.cs b/src/UnitTests.EntityFrameworkCore/src/ITestStore.cs new file mode 100644 index 0000000..0b61087 --- /dev/null +++ b/src/UnitTests.EntityFrameworkCore/src/ITestStore.cs @@ -0,0 +1,14 @@ + +// Copyright (c) Fusonic GmbH. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for license information. + +namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore; + +public interface ITestStore +{ + /// Gets called in the constructor of a DatabaseUnitTest + void OnTestConstruction(); + + /// Gets called in the `DisposeAsync` method of a `DatabaseUnitTest` + Task OnTestEnd(); +} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore/src/SimpleInjectorExtensions.cs b/src/UnitTests.EntityFrameworkCore/src/SimpleInjectorExtensions.cs deleted file mode 100644 index 0719517..0000000 --- a/src/UnitTests.EntityFrameworkCore/src/SimpleInjectorExtensions.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Fusonic.Extensions.UnitTests.SimpleInjector; -using Fusonic.Extensions.XUnit.Logging; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using SimpleInjector; - -namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore; - -public static class SimpleInjectorExtensions -{ - /// - /// Registers a DbContext where each test gets an own database. Note: You need to additionally register an to support your specific database. - /// - /// The type of the EF Core DbContext to register - /// The type of the ITestDatabaseProvider responsible for creating and dropping a test database within a single test. - /// The simple injector container. - /// Configure the DbContextOptionsBuilder. Parameters are the name of the test database and the builder to configure. - /// If enabled, the dbContext logs to the test output. Can be helpful for debugging. Disabled by default. - public static void RegisterDbContext(this Container container, - Action> configureBuilder, - bool enableDbContextLogging = false) - where TDbContext : DbContext - where TTestDatabaseProvider : class, ITestDatabaseProvider - { - container.RegisterTestScoped(); - - container.RegisterTestScoped(() => - { - var dbProvider = container.GetInstance(); - var dbName = dbProvider.TestDbName; - - var builder = new DbContextOptionsBuilder(); - configureBuilder(dbName, builder); - - if (enableDbContextLogging) - builder.UseLoggerFactory(new LoggerFactory(new[] { new XUnitLoggerProvider() })); - - builder.AddInterceptors(new CreateDatabaseInterceptor(ctx => dbProvider.CreateDatabase(ctx))); - - return builder.Options; - }); - - container.Register(Lifestyle.Scoped); - } -} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore/test/CreateDatabaseInterceptorTests.cs b/src/UnitTests.EntityFrameworkCore/test/CreateDatabaseInterceptorTests.cs index 7036973..1543357 100644 --- a/src/UnitTests.EntityFrameworkCore/test/CreateDatabaseInterceptorTests.cs +++ b/src/UnitTests.EntityFrameworkCore/test/CreateDatabaseInterceptorTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. +// Copyright (c) Fusonic GmbH. All rights reserved. // Licensed under the MIT License. See LICENSE file in the project root for license information. namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Tests; @@ -16,7 +16,7 @@ public void NoDatabaseAccessInTest_DoesNotCreateDatabase() // No database access, only resolving DbContext }); - GetInstance().CreateDatabaseCalled.Should().BeFalse(); + ((SqliteTestStore)GetInstance()).CreateDatabaseCalled.Should().BeFalse(); } [Fact] @@ -28,6 +28,6 @@ public void DatabaseAccess_CreatesDatabase() ctx.SaveChanges(); }); - GetInstance().CreateDatabaseCalled.Should().BeTrue(); + ((SqliteTestStore)GetInstance()).CreateDatabaseCalled.Should().BeTrue(); } } \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore/test/Migrations/.editorconfig b/src/UnitTests.EntityFrameworkCore/test/Migrations/.editorconfig deleted file mode 100644 index d49f9e4..0000000 --- a/src/UnitTests.EntityFrameworkCore/test/Migrations/.editorconfig +++ /dev/null @@ -1,5 +0,0 @@ -# This .editorconfig disables the analyzers. This can be used for folders containing generated code, like the EF migrations directory. - -[*.cs] - -dotnet_analyzer_diagnostic.category-Style.severity = silent \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore/test/Migrations/CreateTestDatabase.Designer.cs b/src/UnitTests.EntityFrameworkCore/test/Migrations/CreateTestDatabase.Designer.cs deleted file mode 100644 index 56f93c4..0000000 --- a/src/UnitTests.EntityFrameworkCore/test/Migrations/CreateTestDatabase.Designer.cs +++ /dev/null @@ -1,37 +0,0 @@ -// - -#nullable disable - -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Tests.Migrations -{ - [DbContext(typeof(TestDbContext))] - [Migration("20221123101809_CreateTestDatabase")] - partial class CreateTestDatabase - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.0"); - - modelBuilder.Entity("Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.Tests.TestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("TestEntities"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/UnitTests.EntityFrameworkCore/test/Migrations/CreateTestDatabase.cs b/src/UnitTests.EntityFrameworkCore/test/Migrations/CreateTestDatabase.cs deleted file mode 100644 index 40f5dab..0000000 --- a/src/UnitTests.EntityFrameworkCore/test/Migrations/CreateTestDatabase.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -#nullable disable - -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Tests.Migrations -{ - /// - public partial class CreateTestDatabase : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "TestEntities", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_TestEntities", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "TestEntities"); - } - } -} diff --git a/src/UnitTests.EntityFrameworkCore/test/Migrations/TestDbContextModelSnapshot.cs b/src/UnitTests.EntityFrameworkCore/test/Migrations/TestDbContextModelSnapshot.cs deleted file mode 100644 index dde1d92..0000000 --- a/src/UnitTests.EntityFrameworkCore/test/Migrations/TestDbContextModelSnapshot.cs +++ /dev/null @@ -1,36 +0,0 @@ -// -using Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.Tests; -using Fusonic.Extensions.UnitTests.EntityFrameworkCore.Tests; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.Tests.Migrations -{ - [DbContext(typeof(TestDbContext))] - partial class TestDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.0"); - - modelBuilder.Entity("Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql.Tests.TestEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("TestEntities"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/UnitTests.EntityFrameworkCore/test/Properties/AssemblyInfo.cs b/src/UnitTests.EntityFrameworkCore/test/Properties/AssemblyInfo.cs deleted file mode 100644 index 37d8be3..0000000 --- a/src/UnitTests.EntityFrameworkCore/test/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -[assembly: Fusonic.Extensions.XUnit.Framework.FusonicTestFramework] \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore/test/SqliteTestStore.cs b/src/UnitTests.EntityFrameworkCore/test/SqliteTestStore.cs new file mode 100644 index 0000000..2875d29 --- /dev/null +++ b/src/UnitTests.EntityFrameworkCore/test/SqliteTestStore.cs @@ -0,0 +1,65 @@ +// Copyright (c) Fusonic GmbH. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for license information. + +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Tests; + +public class SqliteTestStore : ITestStore, IDisposable +{ + private SqliteConnection? connection; + + public bool CreateDatabaseCalled { get; private set; } + public bool DropDatabaseCalled { get; private set; } + + public string TestDbName { get; private set; } = null!; + public string ConnectionString => $"Data Source={TestDbName};Mode=Memory;Cache=Shared"; + + public async Task CreateDatabase() + { + if (CreateDatabaseCalled) + return; + + CreateDatabaseCalled = true; + await using var dbContext = CreateDbContext(); + await dbContext.Database.EnsureCreatedAsync(); + } + + public void DropDatabase() + { + DropDatabaseCalled = true; + using var dbContext = CreateDbContext(); + dbContext.Database.EnsureDeleted(); + } + + private TestDbContext CreateDbContext() + => new(new DbContextOptionsBuilder() + .UseSqlite(ConnectionString) + .AddInterceptors(new ConnectionOpeningInterceptor(CreateDatabase)) + .Options); + + public void OnTestConstruction() + { + TestDbName = $"Test_{Guid.NewGuid():N}"; + CreateDatabaseCalled = false; + DropDatabaseCalled = false; + + // Need to maintain an open connection spanning a test to avoid dropping the in-memory DB. + connection = new SqliteConnection(ConnectionString); + connection.Open(); + } + + public async Task OnTestEnd() + { + DropDatabase(); + connection?.Close(); + await Task.CompletedTask; + } + + public void Dispose() + { + connection?.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore/test/TestBase.cs b/src/UnitTests.EntityFrameworkCore/test/TestBase.cs index f032c74..b6620f7 100644 --- a/src/UnitTests.EntityFrameworkCore/test/TestBase.cs +++ b/src/UnitTests.EntityFrameworkCore/test/TestBase.cs @@ -1,15 +1,9 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. +// Copyright (c) Fusonic GmbH. All rights reserved. // Licensed under the MIT License. See LICENSE file in the project root for license information. -using Microsoft.Data.Sqlite; - namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Tests; public abstract class TestBase : DatabaseUnitTest { - protected TestBase(TestFixture fixture) : base(fixture) => - // Need to ensure that one connection is open for the test, as otherwise the InMemory-DB would be dropped. - _ = GetInstance(); - - protected override void DropTestDatabase() => Query(ctx => GetInstance().DropDatabase(ctx)); + protected TestBase(TestFixture fixture) : base(fixture) { } } \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore/test/TestDatabaseProvider.cs b/src/UnitTests.EntityFrameworkCore/test/TestDatabaseProvider.cs deleted file mode 100644 index 0c7aa75..0000000 --- a/src/UnitTests.EntityFrameworkCore/test/TestDatabaseProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Microsoft.EntityFrameworkCore; - -namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Tests; - -public class TestDatabaseProvider : ITestDatabaseProvider -{ - public bool CreateDatabaseCalled { get; private set; } - public bool DropDatabaseCalled { get; private set; } - - public string TestDbName { get; } = $"Test_{Guid.NewGuid():N}"; - - public void CreateDatabase(DbContext dbContext) - { - CreateDatabaseCalled = true; - dbContext.Database.EnsureCreated(); - dbContext.Database.Migrate(); - } - - public void DropDatabase(DbContext dbContext) - { - DropDatabaseCalled = true; - dbContext.Database.EnsureDeleted(); - } -} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore/test/TestDbContext.cs b/src/UnitTests.EntityFrameworkCore/test/TestDbContext.cs index 6666e48..7f3b918 100644 --- a/src/UnitTests.EntityFrameworkCore/test/TestDbContext.cs +++ b/src/UnitTests.EntityFrameworkCore/test/TestDbContext.cs @@ -10,5 +10,5 @@ public class TestDbContext : DbContext public TestDbContext(DbContextOptions options) : base(options) { } - public DbSet TestEntities { get; set; } = null!; + public DbSet TestEntities => Set(); } \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore/test/TestDbContextDesignTimeFactory.cs b/src/UnitTests.EntityFrameworkCore/test/TestDbContextDesignTimeFactory.cs deleted file mode 100644 index a84d826..0000000 --- a/src/UnitTests.EntityFrameworkCore/test/TestDbContextDesignTimeFactory.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; - -namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Tests; - -public class TestDbContextDesignTimeFactory : IDesignTimeDbContextFactory -{ - public TestDbContext CreateDbContext(string[] args) => new(new DbContextOptionsBuilder().UseSqlite("Filename=:memory:").Options); -} \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore/test/TestFixture.cs b/src/UnitTests.EntityFrameworkCore/test/TestFixture.cs index 891b891..79c27ee 100644 --- a/src/UnitTests.EntityFrameworkCore/test/TestFixture.cs +++ b/src/UnitTests.EntityFrameworkCore/test/TestFixture.cs @@ -1,28 +1,20 @@ // Copyright (c) Fusonic GmbH. All rights reserved. // Licensed under the MIT License. See LICENSE file in the project root for license information. -using Fusonic.Extensions.UnitTests.SimpleInjector; -using Microsoft.Data.Sqlite; +using Fusonic.Extensions.UnitTests.ServiceProvider; using Microsoft.EntityFrameworkCore; -using SimpleInjector; +using Microsoft.Extensions.DependencyInjection; namespace Fusonic.Extensions.UnitTests.EntityFrameworkCore.Tests; -public class TestFixture : UnitTestFixture +public class TestFixture : ServiceProviderTestFixture { - protected sealed override void RegisterCoreDependencies(Container container) + protected override void RegisterCoreDependencies(IServiceCollection services) { - container.RegisterDbContext((dbName, builder) => builder.UseSqlite(GetConnectionString(dbName))); + var testStore = new SqliteTestStore(); + services.AddSingleton(testStore); - // Need to maintain an open connection spanning a test to avoid dropping the in-memory DB. - container.RegisterTestScoped(() => - { - var dbName = container.GetInstance().TestDbName; - var connection = new SqliteConnection(GetConnectionString(dbName)); - connection.Open(); - return connection; - }); + services.AddDbContext(b => b.UseSqlite(testStore.ConnectionString) + .AddInterceptors(new ConnectionOpeningInterceptor(testStore.CreateDatabase))); } - - private static string GetConnectionString(string dbName) => $"Data Source={dbName};Mode=Memory;Cache=Shared"; } \ No newline at end of file diff --git a/src/UnitTests.EntityFrameworkCore/test/UnitTests.EntityFrameworkCore.Tests.csproj b/src/UnitTests.EntityFrameworkCore/test/UnitTests.EntityFrameworkCore.Tests.csproj index 238e425..f94fba1 100644 --- a/src/UnitTests.EntityFrameworkCore/test/UnitTests.EntityFrameworkCore.Tests.csproj +++ b/src/UnitTests.EntityFrameworkCore/test/UnitTests.EntityFrameworkCore.Tests.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/UnitTests.ServiceProvider/src/ServiceProviderTestFixture.cs b/src/UnitTests.ServiceProvider/src/ServiceProviderTestFixture.cs new file mode 100644 index 0000000..b84c435 --- /dev/null +++ b/src/UnitTests.ServiceProvider/src/ServiceProviderTestFixture.cs @@ -0,0 +1,50 @@ +// Copyright (c) Fusonic GmbH. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for license information. + +using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection; + +namespace Fusonic.Extensions.UnitTests.ServiceProvider; + +public abstract class ServiceProviderTestFixture : DependencyInjectionTestFixture, IDisposable +{ + private readonly IServiceProvider serviceProvider; + + protected virtual ServiceProviderOptions ServiceProviderOptions { get; } = new() + { + ValidateOnBuild = true, + ValidateScopes = true + }; + + protected ServiceProviderTestFixture() => serviceProvider = BuildServiceProvider(); + + [DebuggerStepThrough] + public sealed override AsyncServiceScope BeginScope() => serviceProvider.CreateAsyncScope(); + + [DebuggerStepThrough] + public sealed override T GetInstance(AsyncServiceScope scope) => scope.ServiceProvider.GetRequiredService(); + + [DebuggerStepThrough] + public override object GetInstance(AsyncServiceScope scope, Type serviceType) => scope.ServiceProvider.GetRequiredService(serviceType); + + private IServiceProvider BuildServiceProvider() + { + var services = new ServiceCollection(); + RegisterCoreDependencies(services); + RegisterDependencies(services); + + return services.BuildServiceProvider(ServiceProviderOptions); + } + + protected virtual void RegisterCoreDependencies(IServiceCollection services) { } + + protected virtual void RegisterDependencies(IServiceCollection services) { } + + public void Dispose() + { + if (serviceProvider is IDisposable d) + d.Dispose(); + + GC.SuppressFinalize(this); + } +} diff --git a/src/UnitTests.ServiceProvider/src/UnitTests.ServiceProvider.csproj b/src/UnitTests.ServiceProvider/src/UnitTests.ServiceProvider.csproj new file mode 100644 index 0000000..33c6cf4 --- /dev/null +++ b/src/UnitTests.ServiceProvider/src/UnitTests.ServiceProvider.csproj @@ -0,0 +1,22 @@ + + + Fusonic.Extensions.UnitTests.ServiceProvider + Fusonic.Extensions.UnitTests.ServiceProvider + https://github.com/fusonic/dotnet-extensions + true + snupkg + Xunit-based testing base classes. Supports dependency injection with Microsofts Dependency Injection framework (ServiceProvider). + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + + false + + + + + + + + + + + diff --git a/src/UnitTests.SimpleInjector/src/SimpleInjectorTestFixture.cs b/src/UnitTests.SimpleInjector/src/SimpleInjectorTestFixture.cs new file mode 100644 index 0000000..af933df --- /dev/null +++ b/src/UnitTests.SimpleInjector/src/SimpleInjectorTestFixture.cs @@ -0,0 +1,48 @@ +// Copyright (c) Fusonic GmbH. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for license information. + +using System.Diagnostics; +using SimpleInjector; +using SimpleInjector.Lifestyles; + +namespace Fusonic.Extensions.UnitTests.SimpleInjector; + +public abstract class SimpleInjectorTestFixture : DependencyInjectionTestFixture, IDisposable +{ + private readonly Container container = new(); + protected virtual bool VerifyContainer => true; + + protected SimpleInjectorTestFixture() => ConfigureContainer(); + + private void ConfigureContainer() + { + container.Options.DefaultLifestyle = Lifestyle.Scoped; + container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle(); + container.Options.AllowOverridingRegistrations = true; + + RegisterCoreDependencies(container); + RegisterDependencies(container); + + if (VerifyContainer) + container.Verify(); + } + + protected virtual void RegisterCoreDependencies(Container container) { } + + protected virtual void RegisterDependencies(Container container) { } + + [DebuggerStepThrough] + public sealed override Scope BeginScope() => AsyncScopedLifestyle.BeginScope(container); + + [DebuggerStepThrough] + public sealed override T GetInstance(Scope scope) => scope.Container!.GetInstance(); + + [DebuggerStepThrough] + public override object GetInstance(Scope scope, Type serviceType) => scope.Container!.GetInstance(serviceType); + + public virtual void Dispose() + { + container.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/src/UnitTests.SimpleInjector/src/UnitTests.SimpleInjector.csproj b/src/UnitTests.SimpleInjector/src/UnitTests.SimpleInjector.csproj new file mode 100644 index 0000000..2663047 --- /dev/null +++ b/src/UnitTests.SimpleInjector/src/UnitTests.SimpleInjector.csproj @@ -0,0 +1,22 @@ + + + Fusonic.Extensions.UnitTests.SimpleInjector + Fusonic.Extensions.UnitTests.SimpleInjector + https://github.com/fusonic/dotnet-extensions + true + snupkg + Xunit-based testing base classes. Supports dependency injection with SimpleInjector. + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + + false + + + + + + + + + + + diff --git a/src/UnitTests/src/DependencyInjectionTestFixture.cs b/src/UnitTests/src/DependencyInjectionTestFixture.cs new file mode 100644 index 0000000..e3d89c1 --- /dev/null +++ b/src/UnitTests/src/DependencyInjectionTestFixture.cs @@ -0,0 +1,27 @@ +// Copyright (c) Fusonic GmbH. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for license information. + +using Microsoft.Extensions.Configuration; + +namespace Fusonic.Extensions.UnitTests; + +public abstract class DependencyInjectionTestFixture : IDependencyInjectionTestFixture +where TScope : notnull +{ + private static IConfiguration? defaultConfiguration; + public IConfiguration Configuration { get; } + + protected DependencyInjectionTestFixture() => Configuration = BuildConfiguration(); + + protected virtual IConfiguration BuildConfiguration() + => defaultConfiguration + ??= TestConfigurationHelper.GetDefaultConfiguration(Directory.GetCurrentDirectory(), GetType().Assembly); + + public abstract TScope BeginScope(); + public abstract object GetInstance(TScope scope, Type serviceType); + public abstract T GetInstance(TScope scope) where T : class; + + object IDependencyInjectionTestFixture.BeginScope() => BeginScope(); + object IDependencyInjectionTestFixture.GetInstance(object scope, Type serviceType) => GetInstance((TScope)scope, serviceType); + T IDependencyInjectionTestFixture.GetInstance(object scope) => GetInstance((TScope)scope); +} \ No newline at end of file diff --git a/src/UnitTests/src/UnitTest.cs b/src/UnitTests/src/DependencyInjectionUnitTest.cs similarity index 65% rename from src/UnitTests/src/UnitTest.cs rename to src/UnitTests/src/DependencyInjectionUnitTest.cs index 50e91aa..dd381d7 100644 --- a/src/UnitTests/src/UnitTest.cs +++ b/src/UnitTests/src/DependencyInjectionUnitTest.cs @@ -2,37 +2,36 @@ // Licensed under the MIT License. See LICENSE file in the project root for license information. using System.Diagnostics; -using Fusonic.Extensions.UnitTests.SimpleInjector; using MediatR; -using SimpleInjector; using Xunit; namespace Fusonic.Extensions.UnitTests; -public abstract class UnitTest : IDisposable, IClassFixture - where TFixture : UnitTestFixture +public abstract class DependencyInjectionUnitTest : IDisposable, IClassFixture + where TFixture : class, IDependencyInjectionTestFixture { - private Scope currentScope; - + private object currentScope; protected TFixture Fixture { get; } - protected Container Container => currentScope.Container!; - protected UnitTest(TFixture fixture) + protected DependencyInjectionUnitTest(TFixture fixture) { Fixture = fixture; - currentScope = fixture.BeginLifetimeScope(); + currentScope = fixture.BeginScope(); } - /// Shortcut for Container.GetInstance{T} + /// Gets an instance of the requested service. [DebuggerStepThrough] protected T GetInstance() - where T : class - => Container.GetInstance(); + where T : class => Fixture.GetInstance(currentScope); + + /// Gets an instance of the requested service type. + [DebuggerStepThrough] + protected object GetInstance(Type serviceType) => Fixture.GetInstance(currentScope, serviceType); /// Runs a mediator command in its own scope. Used to reduce possible side effects from test data creation and the like. [DebuggerStepThrough] protected Task SendAsync(IRequest request) - => ScopedAsync(() => Container.GetInstance().Send(request)); + => ScopedAsync(() => GetInstance().Send(request)); /// [DebuggerStepThrough] @@ -41,9 +40,17 @@ protected TResult Scoped(Func action) var prevScope = currentScope; try { - using var newScope = Fixture.BeginLifetimeScope(); + var newScope = Fixture.BeginScope(); currentScope = newScope; - return action(); + try + { + return action(); + } + finally + { + if (newScope is IDisposable d) + d.Dispose(); + } } finally { @@ -58,7 +65,7 @@ protected async Task ScopedAsync(Func> taskFacto var prevScope = currentScope; try { - await using var newScope = Fixture.BeginLifetimeScope(); + var newScope = Fixture.BeginScope(); currentScope = newScope; return await taskFactory(); } @@ -86,8 +93,7 @@ protected Task ScopedAsync(Func taskFactory) => ScopedAsync(async () => public virtual void Dispose() { - currentScope.Dispose(); - TestScopedLifestyle.CleanupTestScopes(); + (currentScope as IDisposable)?.Dispose(); GC.SuppressFinalize(this); } -} +} \ No newline at end of file diff --git a/src/UnitTests/src/IDependencyInjectionTestFixture.cs b/src/UnitTests/src/IDependencyInjectionTestFixture.cs new file mode 100644 index 0000000..bad3430 --- /dev/null +++ b/src/UnitTests/src/IDependencyInjectionTestFixture.cs @@ -0,0 +1,11 @@ +// Copyright (c) Fusonic GmbH. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for license information. + +namespace Fusonic.Extensions.UnitTests; + +public interface IDependencyInjectionTestFixture +{ + object BeginScope(); + object GetInstance(object scope, Type type); + T GetInstance(object scope) where T : class; +} \ No newline at end of file diff --git a/src/UnitTests/src/SimpleInjector/ContainerExtensions.cs b/src/UnitTests/src/SimpleInjector/ContainerExtensions.cs deleted file mode 100644 index 258c9d7..0000000 --- a/src/UnitTests/src/SimpleInjector/ContainerExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using SimpleInjector; - -namespace Fusonic.Extensions.UnitTests.SimpleInjector; - -public static class ContainerExtensions -{ - private static readonly Lifestyle TestScopedLifestyle = new TestScopedLifestyle(); - - public static void RegisterTestScoped(this Container container) - where TConcrete : class - => container.Register(TestScopedLifestyle); - - public static void RegisterTestScoped(this Container container) - where TService : class - where TServiceImplementation : class, TService - => container.Register(TestScopedLifestyle); - - public static void RegisterTestScoped(this Container container, Func instanceCreator) - where TService : class - => container.Register(instanceCreator, TestScopedLifestyle); -} diff --git a/src/UnitTests/src/SimpleInjector/TestScopedLifestyle.cs b/src/UnitTests/src/SimpleInjector/TestScopedLifestyle.cs deleted file mode 100644 index f809189..0000000 --- a/src/UnitTests/src/SimpleInjector/TestScopedLifestyle.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Fusonic.Extensions.XUnit; -using Fusonic.Extensions.XUnit.Framework; -using SimpleInjector; - -namespace Fusonic.Extensions.UnitTests.SimpleInjector; - -/// -/// Lifestyle per test context. -/// The whole thing is based on SimpleInjector.Integration.Web.WebRequestLifestyle, which fortunately does the exact same thing. It just uses HttpContext.Current instead of TestContext.Current. -/// -public sealed class TestScopedLifestyle : ScopedLifestyle -{ - private static readonly object ScopeKey = new(); - - public TestScopedLifestyle() : base(nameof(TestScopedLifestyle)) - { } - - internal static void CleanupTestScopes() - { - if (TestContext.Items[ScopeKey] is List scopes) - DisposeScopes(scopes); - } - - protected override Scope? GetCurrentScopeCore(Container container) => GetOrCreateScope(container); - protected override Func CreateCurrentScopeProvider(Container container) => () => GetOrCreateScope(container); - - private static Scope? GetOrCreateScope(Container container) - { - if (!TestContext.IsSet) - throw new InvalidOperationException($"The test context is not set. This indicates that you did not set the {nameof(FusonicTestFrameworkAttribute)} in your assembly."); - - if (TestContext.Items[ScopeKey] is not List scopes) - TestContext.Items[ScopeKey] = scopes = new List(capacity: 2); - - var scope = FindScopeForContainer(scopes, container); - if (scope is null) - scopes.Add(scope = new Scope(container)); - - return scope; - } - - private static Scope? FindScopeForContainer(List scopes, Container container) - { - foreach (var scope in scopes) - { - if (scope.Container == container) - return scope; - } - - return null; - } - - private static void DisposeScopes(List scopes) - { - if (scopes.Count == 1) - scopes[0].Dispose(); - else if (scopes.Count > 1) - DisposeScopesInReverseOrder(scopes); - } - - private static void DisposeScopesInReverseOrder(List scopes) - { - // Here we use a 'master' scope that will hold the real scopes. This allows all scopes - // to be disposed, even if a scope's Dispose method throws an exception. Scopes will - // also be disposed in opposite order of creation. - using var masterScope = new Scope(scopes[0].Container!); - foreach (var scope in scopes) - { - masterScope.RegisterForDisposal((IAsyncDisposable)scope); - } - } -} diff --git a/src/UnitTests/src/TestConfigurationHelper.cs b/src/UnitTests/src/TestConfigurationHelper.cs new file mode 100644 index 0000000..ec0f50d --- /dev/null +++ b/src/UnitTests/src/TestConfigurationHelper.cs @@ -0,0 +1,21 @@ +// Copyright (c) Fusonic GmbH. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for license information. + +using System.Reflection; +using Microsoft.Extensions.Configuration; + +namespace Fusonic.Extensions.UnitTests; + +public static class TestConfigurationHelper +{ + public static IConfigurationBuilder ConfigureTestDefault(this IConfigurationBuilder builder, string basePath, Assembly assembly) + => builder.SetBasePath(basePath) + .AddJsonFile("testsettings.json", optional: true) + .AddUserSecrets(assembly, optional: true) + .AddEnvironmentVariables(); + + public static IConfiguration GetDefaultConfiguration(string basePath, Assembly assembly) + => new ConfigurationBuilder() + .ConfigureTestDefault(basePath, assembly) + .Build(); +} \ No newline at end of file diff --git a/src/UnitTests/src/UnitTestFixture.cs b/src/UnitTests/src/UnitTestFixture.cs deleted file mode 100644 index 2551234..0000000 --- a/src/UnitTests/src/UnitTestFixture.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using System.Diagnostics; -using System.Reflection; -using Fusonic.Extensions.XUnit.Framework; -using Microsoft.Extensions.Configuration; -using SimpleInjector; -using SimpleInjector.Lifestyles; - -namespace Fusonic.Extensions.UnitTests; - -public abstract class UnitTestFixture : IDisposable -{ - private readonly Container container = new(); - - protected virtual bool VerifyContainer { get; } = true; - - public IConfiguration Configuration { get; } - - protected UnitTestFixture() - { - Configuration = BuildConfiguration(); - ConfigureContainer(); - } - - public static IConfiguration GetDefaultConfiguration(string basePath, Assembly assembly) - => new ConfigurationBuilder() - .SetBasePath(basePath) - .AddJsonFile("testsettings.json", optional: true) - .AddUserSecrets(assembly, optional: true) - .AddEnvironmentVariables() - .Build(); - - protected virtual IConfiguration BuildConfiguration() => GetDefaultConfiguration(Directory.GetCurrentDirectory(), GetType().Assembly); - - private void ConfigureContainer() - { - if (GetType().Assembly.GetCustomAttribute() == null) - throw new InvalidOperationException($"{nameof(FusonicTestFrameworkAttribute)} must be set in test assembly '{GetType().Assembly.FullName}'."); - - container.Options.DefaultLifestyle = Lifestyle.Scoped; - container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle(); - container.Options.AllowOverridingRegistrations = true; - - RegisterCoreDependencies(container); - RegisterDependencies(container); - - if (VerifyContainer) - container.Verify(); - } - - protected virtual void RegisterCoreDependencies(Container container) { } - - protected virtual void RegisterDependencies(Container container) { } - - [DebuggerStepThrough] - public Scope BeginLifetimeScope() => AsyncScopedLifestyle.BeginScope(container); - - public virtual void Dispose() - { - container.Dispose(); - GC.SuppressFinalize(this); - } -} \ No newline at end of file diff --git a/src/UnitTests/src/UnitTests.csproj b/src/UnitTests/src/UnitTests.csproj index c24bc40..bda38ea 100644 --- a/src/UnitTests/src/UnitTests.csproj +++ b/src/UnitTests/src/UnitTests.csproj @@ -5,7 +5,7 @@ https://github.com/fusonic/dotnet-extensions true snupkg - Xunit-based testing base classes. Supports DI with SimpleInjector, MediatR-event-recordings, Lifetime-Scoped calls, a test context and so on. + Xunit-based testing base classes with support for dependency injection. Libraries supporting specific DI containers (SimpleInjector, ServiceProvider) are in separate packages. bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml false @@ -18,14 +18,11 @@ + - - - - diff --git a/src/XUnit/src/Framework/BeforeAfterTestInvokeAttribute.cs b/src/XUnit/src/Framework/BeforeAfterTestInvokeAttribute.cs deleted file mode 100644 index f06024c..0000000 --- a/src/XUnit/src/Framework/BeforeAfterTestInvokeAttribute.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using System.Reflection; - -namespace Fusonic.Extensions.XUnit.Framework; - -/// -/// Base attribute which indicates a test method interception. This allows code to be run before and after the test is run. -/// -/// The difference to the Xunit.Sdk.BeforeAfterTestAttribute is that the XUnit version executes the 'Before' after the test class is created and the constructor ran. -/// This version runs before the test class is created, allowing to create something like a test context before the constructor gets called. -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)] -public abstract class BeforeAfterTestInvokeAttribute : Attribute -{ - /// - /// This method is called before the test class is created and the method is executed. - /// - /// The method under test - public virtual void Before(MethodInfo methodUnderTest) - { } - - /// - /// This method is called after the test method is executed. - /// - /// The method under test - public virtual void After(MethodInfo methodUnderTest) - { } -} diff --git a/src/XUnit/src/Framework/FusonicFactDiscoverer.cs b/src/XUnit/src/Framework/FusonicFactDiscoverer.cs deleted file mode 100644 index a472454..0000000 --- a/src/XUnit/src/Framework/FusonicFactDiscoverer.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Fusonic.Extensions.XUnit.Framework; - -public class FusonicFactDiscoverer : FactDiscoverer -{ - public FusonicFactDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) - { } - - protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) - { - return new FusonicTestCase(DiagnosticMessageSink, - discoveryOptions.MethodDisplayOrDefault(), - discoveryOptions.MethodDisplayOptionsOrDefault(), - testMethod); - } -} diff --git a/src/XUnit/src/Framework/FusonicSkippedDataRowTestCase.cs b/src/XUnit/src/Framework/FusonicSkippedDataRowTestCase.cs deleted file mode 100644 index 96ef195..0000000 --- a/src/XUnit/src/Framework/FusonicSkippedDataRowTestCase.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Fusonic.Extensions.XUnit.Framework; - -public class FusonicSkippedDataRowTestCase : XunitSkippedDataRowTestCase -{ -#pragma warning disable 618 //ctor is marked with obsolete, but is required by serializer. See base class. - public FusonicSkippedDataRowTestCase() - { } -#pragma warning restore 618 - - public FusonicSkippedDataRowTestCase( - IMessageSink diagnosticMessageSink, - TestMethodDisplay defaultMethodDisplay, - TestMethodDisplayOptions defaultMethodDisplayOptions, - ITestMethod testMethod, - string skipReason, - object[]? testMethodArguments = null) : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, skipReason, testMethodArguments) - { } - - public override Task RunAsync( - IMessageSink diagnosticMessageSink, - IMessageBus messageBus, - object[] constructorArguments, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) - { - return new FusonicTestCaseRunner(this, DisplayName, SkipReason, constructorArguments, TestMethodArguments, messageBus, aggregator, cancellationTokenSource) - .RunAsync(); - } -} diff --git a/src/XUnit/src/Framework/FusonicTestAssemblyRunner.cs b/src/XUnit/src/Framework/FusonicTestAssemblyRunner.cs deleted file mode 100644 index 8f1f22f..0000000 --- a/src/XUnit/src/Framework/FusonicTestAssemblyRunner.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Fusonic.Extensions.XUnit.Framework; - -public class FusonicTestAssemblyRunner : XunitTestAssemblyRunner -{ - private bool isInitialized; - private SemaphoreSlim? testCollectionSemaphore; - - public FusonicTestAssemblyRunner(ITestAssembly testAssembly, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions) - { } - - protected override Task AfterTestAssemblyStartingAsync() - { - Init(); - return base.AfterTestAssemblyStartingAsync(); - } - - private void Init() - { - if (isInitialized) - return; - - // Try get maxParallelTests value from the environment - var env = Environment.GetEnvironmentVariable("MAX_PARALLEL_TESTS"); - - int maxParallelTests; - if (env != null && int.TryParse(env, out var max)) - { - maxParallelTests = max; - } - // Take value from assembly attribute setting, which defaults to 0. - else - { - var fusonicTestFrameworkAttribute = TestAssembly.Assembly.GetCustomAttributes(typeof(FusonicTestFrameworkAttribute)).Single(); - maxParallelTests = fusonicTestFrameworkAttribute.GetNamedArgument(nameof(FusonicTestFrameworkAttribute.MaxParallelTests)); - } - - if (maxParallelTests == 0) - maxParallelTests = Environment.ProcessorCount; - - if (maxParallelTests > 0) - testCollectionSemaphore = new SemaphoreSlim(maxParallelTests); - - isInitialized = true; - } - - protected override async Task RunTestCollectionAsync(IMessageBus messageBus, ITestCollection testCollection, IEnumerable testCases, CancellationTokenSource cancellationTokenSource) - { - if (testCollectionSemaphore == null) - return await base.RunTestCollectionAsync(messageBus, testCollection, testCases, cancellationTokenSource); - - try - { - var cancellationToken = cancellationTokenSource.Token; - await testCollectionSemaphore.WaitAsync(cancellationToken); - - cancellationToken.ThrowIfCancellationRequested(); - - return await base.RunTestCollectionAsync(messageBus, testCollection, testCases, cancellationTokenSource); - } - finally - { - testCollectionSemaphore.Release(); - } - } -} \ No newline at end of file diff --git a/src/XUnit/src/Framework/FusonicTestCase.cs b/src/XUnit/src/Framework/FusonicTestCase.cs deleted file mode 100644 index 34275e6..0000000 --- a/src/XUnit/src/Framework/FusonicTestCase.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Fusonic.Extensions.XUnit.Framework; - -public class FusonicTestCase : XunitTestCase -{ -#pragma warning disable 618 //ctor is marked with obsolete, but is required by serializer. See base class. - public FusonicTestCase() - { } -#pragma warning restore 618 - - public FusonicTestCase( - IMessageSink diagnosticMessageSink, - TestMethodDisplay defaultMethodDisplay, - TestMethodDisplayOptions defaultMethodDisplayOptions, - ITestMethod testMethod, - object[]? testMethodArguments = null) : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) - { } - - public override Task RunAsync( - IMessageSink diagnosticMessageSink, - IMessageBus messageBus, - object[] constructorArguments, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) - { - return new FusonicTestCaseRunner(this, DisplayName, SkipReason, constructorArguments, TestMethodArguments, messageBus, aggregator, cancellationTokenSource) - .RunAsync(); - } -} diff --git a/src/XUnit/src/Framework/FusonicTestCaseRunner.cs b/src/XUnit/src/Framework/FusonicTestCaseRunner.cs deleted file mode 100644 index ec73b3a..0000000 --- a/src/XUnit/src/Framework/FusonicTestCaseRunner.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using System.Reflection; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Fusonic.Extensions.XUnit.Framework; - -public class FusonicTestCaseRunner : XunitTestCaseRunner -{ - public FusonicTestCaseRunner( - IXunitTestCase testCase, - string displayName, - string skipReason, - object[] constructorArguments, - object[] testMethodArguments, - IMessageBus messageBus, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) : base(testCase, displayName, skipReason, constructorArguments, testMethodArguments, messageBus, aggregator, cancellationTokenSource) - { } - - protected override XunitTestRunner CreateTestRunner( - ITest test, - IMessageBus messageBus, - Type testClass, - object[] constructorArguments, - MethodInfo testMethod, - object[] testMethodArguments, - string skipReason, - IReadOnlyList beforeAfterAttributes, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) - { - return new FusonicTestRunner(test, - messageBus, - testClass, - constructorArguments, - testMethod, - testMethodArguments, - skipReason, - beforeAfterAttributes, - new ExceptionAggregator(aggregator), - cancellationTokenSource); - } -} diff --git a/src/XUnit/src/Framework/FusonicTestFramework.cs b/src/XUnit/src/Framework/FusonicTestFramework.cs deleted file mode 100644 index 1db2feb..0000000 --- a/src/XUnit/src/Framework/FusonicTestFramework.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using System.Reflection; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Fusonic.Extensions.XUnit.Framework; - -public class FusonicTestFramework : XunitTestFramework -{ - public FusonicTestFramework(IMessageSink messageSink) : base(messageSink) - { } - - /// - /// This is more or less the starting point of the whole overwrite-pipe. It runs through a lot of overwritten classes only returning our Fusonic-Versions, that don't have any extra logic. - /// The FusonicTestRunner is the one that creates the test context and calls our own before/after test attributes. - /// - protected override ITestFrameworkDiscoverer CreateDiscoverer(IAssemblyInfo assemblyInfo) - => new FusonicTestFrameworkDiscoverer(assemblyInfo, SourceInformationProvider, DiagnosticMessageSink); - - /// - /// The executor uses another assembly runner that can limit the maximum amount of parallel tests, in addition to the maximum amount of threads provided by XUnit. - /// - protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName) - => new FusonicTestFrameworkExecutor(assemblyName, SourceInformationProvider, DiagnosticMessageSink); -} \ No newline at end of file diff --git a/src/XUnit/src/Framework/FusonicTestFrameworkAttribute.cs b/src/XUnit/src/Framework/FusonicTestFrameworkAttribute.cs deleted file mode 100644 index 3d0ab58..0000000 --- a/src/XUnit/src/Framework/FusonicTestFrameworkAttribute.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Fusonic.Extensions.XUnit.Framework; - -/// -/// Used to decorate an assembly to allow the use of a custom . -/// -[TestFrameworkDiscoverer("Fusonic.Extensions.XUnit.Framework." + nameof(FusonicTestFrameworkTypeDiscoverer), "Fusonic.Extensions.XUnit")] -[AttributeUsage(AttributeTargets.Assembly)] -public sealed class FusonicTestFrameworkAttribute : Attribute, ITestFrameworkAttribute -{ - /// - /// Gets the maximum number of tests running in parallel. - /// When using MaxParallelThreads from XUnit, it limits the number of maximum active tests executing, but it does not limit of maximum parallel tests started. - /// 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 option limits the number of tests that get started in parallel. - /// If set to 0, the system will use . - /// If set to a negative number, then there will be no limit to the number of tests. - /// Defaults to 0. - ///
- public int MaxParallelTests { get; set; } -} \ No newline at end of file diff --git a/src/XUnit/src/Framework/FusonicTestFrameworkDiscoverer.cs b/src/XUnit/src/Framework/FusonicTestFrameworkDiscoverer.cs deleted file mode 100644 index bc48463..0000000 --- a/src/XUnit/src/Framework/FusonicTestFrameworkDiscoverer.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Xunit; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Fusonic.Extensions.XUnit.Framework; - -public class FusonicTestFrameworkDiscoverer : XunitTestFrameworkDiscoverer -{ - public FusonicTestFrameworkDiscoverer( - IAssemblyInfo assemblyInfo, - ISourceInformationProvider sourceProvider, - IMessageSink diagnosticMessageSink, - IXunitTestCollectionFactory? collectionFactory = null) : base(assemblyInfo, sourceProvider, diagnosticMessageSink, collectionFactory) - { - DiscovererTypeCache[typeof(FactAttribute)] = typeof(FusonicFactDiscoverer); - DiscovererTypeCache[typeof(TheoryAttribute)] = typeof(FusonicTheoryDiscoverer); - } -} diff --git a/src/XUnit/src/Framework/FusonicTestFrameworkExecutor.cs b/src/XUnit/src/Framework/FusonicTestFrameworkExecutor.cs deleted file mode 100644 index f7822e2..0000000 --- a/src/XUnit/src/Framework/FusonicTestFrameworkExecutor.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using System.Reflection; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Fusonic.Extensions.XUnit.Framework; - -public class FusonicTestFrameworkExecutor : XunitTestFrameworkExecutor -{ - public FusonicTestFrameworkExecutor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider, IMessageSink diagnosticMessageSink) : base(assemblyName, sourceInformationProvider, diagnosticMessageSink) - { } - - protected override async void RunTestCases(IEnumerable testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions) - { - using var assemblyRunner = new FusonicTestAssemblyRunner(TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, executionOptions); - await assemblyRunner.RunAsync(); - } -} \ No newline at end of file diff --git a/src/XUnit/src/Framework/FusonicTestFrameworkTypeDiscoverer.cs b/src/XUnit/src/Framework/FusonicTestFrameworkTypeDiscoverer.cs deleted file mode 100644 index dc7dba6..0000000 --- a/src/XUnit/src/Framework/FusonicTestFrameworkTypeDiscoverer.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Fusonic.Extensions.XUnit.Framework; - -public class FusonicTestFrameworkTypeDiscoverer : ITestFrameworkTypeDiscoverer -{ - public Type GetTestFrameworkType(IAttributeInfo attribute) - => typeof(FusonicTestFramework); -} diff --git a/src/XUnit/src/Framework/FusonicTestRunner.cs b/src/XUnit/src/Framework/FusonicTestRunner.cs deleted file mode 100644 index a75758e..0000000 --- a/src/XUnit/src/Framework/FusonicTestRunner.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using System.Reflection; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Fusonic.Extensions.XUnit.Framework; - -/// -/// Runs the test methods. This class is additionally responsible for creating the TestContext, handling the test output and running the BeforeAfterInvokeAttribute-Methods. -/// -public class FusonicTestRunner : XunitTestRunner -{ - private readonly List beforeAfterInvokeAttributes = new(); - private readonly ITestOutputHelper? testOutputHelper; - - public FusonicTestRunner( - ITest test, - IMessageBus messageBus, - Type testClass, - object[] constructorArguments, - MethodInfo testMethod, - object[] testMethodArguments, - string skipReason, - IReadOnlyList beforeAfterAttributes, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) - : base(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource) - { - //First get the attributes on the class, then on the method. Method attributes should get executed after the class attributes. - beforeAfterInvokeAttributes.AddRange(testClass.GetCustomAttributes(inherit: true)); - beforeAfterInvokeAttributes.AddRange(testMethod.GetCustomAttributes()); - - //If there's a ITestOutputHelper in the ctor, we use that one instead of creating an own. - testOutputHelper = constructorArguments.OfType().FirstOrDefault(); - } - - protected override async Task> InvokeTestAsync(ExceptionAggregator aggregator) - { - //if the output helper wasn't in the ctor, we create our own. - TestOutputHelper? ownOutputHelper = null; - if (testOutputHelper == null) - { - ownOutputHelper = new TestOutputHelper(); - ownOutputHelper.Initialize(MessageBus, Test); - } - - using (TestContext.Create(testOutputHelper ?? ownOutputHelper!, TestMethod, TestClass)) - { - BeforeTestCaseInvoked(); - var result = await base.InvokeTestAsync(aggregator); - AfterTestCaseInvoked(); - - //if we own the output helper, set the output in the result - if (ownOutputHelper != null) - { - result = new Tuple(result.Item1, ownOutputHelper.Output); - ownOutputHelper.Uninitialize(); - } - - return result; - } - } - - private void BeforeTestCaseInvoked() - { - foreach (var attribute in beforeAfterInvokeAttributes) - { - attribute.Before(TestMethod); - } - } - - private void AfterTestCaseInvoked() - { - foreach (var attribute in beforeAfterInvokeAttributes) - { - attribute.After(TestMethod); - } - } -} diff --git a/src/XUnit/src/Framework/FusonicTheoryDiscoverer.cs b/src/XUnit/src/Framework/FusonicTheoryDiscoverer.cs deleted file mode 100644 index 6f392ca..0000000 --- a/src/XUnit/src/Framework/FusonicTheoryDiscoverer.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Fusonic.Extensions.XUnit.Framework; - -public class FusonicTheoryDiscoverer : TheoryDiscoverer -{ - public FusonicTheoryDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) - { } - - protected override IEnumerable CreateTestCasesForDataRow( - ITestFrameworkDiscoveryOptions discoveryOptions, - ITestMethod testMethod, - IAttributeInfo theoryAttribute, - object[] dataRow) - { - return new[] { new FusonicTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, dataRow) }; - } - - protected override IEnumerable CreateTestCasesForSkip( - ITestFrameworkDiscoveryOptions discoveryOptions, - ITestMethod testMethod, - IAttributeInfo theoryAttribute, - string skipReason) - { - return new[] { new FusonicTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod) }; - } - - protected override IEnumerable CreateTestCasesForSkippedDataRow( - ITestFrameworkDiscoveryOptions discoveryOptions, - ITestMethod testMethod, - IAttributeInfo theoryAttribute, - object[] dataRow, - string skipReason) - { - return new[] - { - new FusonicSkippedDataRowTestCase(DiagnosticMessageSink, - discoveryOptions.MethodDisplayOrDefault(), - discoveryOptions.MethodDisplayOptionsOrDefault(), - testMethod, - skipReason, - dataRow) - }; - } - - protected override IEnumerable CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) - { - return new[] { new FusonicTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod) }; - } -} diff --git a/src/XUnit/src/Framework/FusonicTheoryTestCase.cs b/src/XUnit/src/Framework/FusonicTheoryTestCase.cs deleted file mode 100644 index 2b17c13..0000000 --- a/src/XUnit/src/Framework/FusonicTheoryTestCase.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Fusonic.Extensions.XUnit.Framework; - -public class FusonicTheoryTestCase : XunitTheoryTestCase -{ -#pragma warning disable 618 //ctor is marked with obsolete, but is required by serializer. See base class. - public FusonicTheoryTestCase() - { } -#pragma warning restore 618 - - public FusonicTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod) : base( - diagnosticMessageSink, - defaultMethodDisplay, - defaultMethodDisplayOptions, - testMethod) - { } - - public override Task RunAsync( - IMessageSink diagnosticMessageSink, - IMessageBus messageBus, - object[] constructorArguments, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) - { - return new FusonicTheoryTestCaseRunner(this, - DisplayName, - SkipReason, - constructorArguments, - diagnosticMessageSink, - messageBus, - aggregator, - cancellationTokenSource) - .RunAsync(); - } -} diff --git a/src/XUnit/src/Framework/FusonicTheoryTestCaseRunner.cs b/src/XUnit/src/Framework/FusonicTheoryTestCaseRunner.cs deleted file mode 100644 index f5c2211..0000000 --- a/src/XUnit/src/Framework/FusonicTheoryTestCaseRunner.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using System.Reflection; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Fusonic.Extensions.XUnit.Framework; - -public class FusonicTheoryTestCaseRunner : XunitTheoryTestCaseRunner -{ - public FusonicTheoryTestCaseRunner( - IXunitTestCase testCase, - string displayName, - string skipReason, - object[] constructorArguments, - IMessageSink diagnosticMessageSink, - IMessageBus messageBus, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) : base(testCase, displayName, skipReason, constructorArguments, diagnosticMessageSink, messageBus, aggregator, cancellationTokenSource) - { } - - protected override XunitTestRunner CreateTestRunner( - ITest test, - IMessageBus messageBus, - Type testClass, - object[] constructorArguments, - MethodInfo testMethod, - object[] testMethodArguments, - string skipReason, - IReadOnlyList beforeAfterAttributes, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) - { - return new FusonicTestRunner(test, - messageBus, - testClass, - constructorArguments, - testMethod, - testMethodArguments, - skipReason, - beforeAfterAttributes, - new ExceptionAggregator(aggregator), - cancellationTokenSource); - } -} diff --git a/src/XUnit/src/Logging/XUnitLogger.cs b/src/XUnit/src/Logging/XUnitLogger.cs deleted file mode 100644 index 6538334..0000000 --- a/src/XUnit/src/Logging/XUnitLogger.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Microsoft.Extensions.Logging; - -namespace Fusonic.Extensions.XUnit.Logging; - -/// -/// Implementation of the Microsoft ILogger. Can be used for several services, for example for EF logs. -/// Logs to the ITestOutputHelper configured in the TestContext. -/// -public class XUnitLogger : ILogger -{ - private readonly string categoryName; - - public XUnitLogger(string categoryName) => this.categoryName = categoryName; - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) - { - TestContext.WriteLine($"{categoryName} [{eventId}] {formatter(state, exception)}"); - if (exception != null) - TestContext.WriteLine(exception.ToString()); - } - - public bool IsEnabled(LogLevel logLevel) => true; - public IDisposable BeginScope(TState state) where TState : notnull => NoScope.Instance; - - private sealed class NoScope : IDisposable - { - public static readonly NoScope Instance = new(); - public void Dispose() - { } - } -} diff --git a/src/XUnit/src/Logging/XUnitLoggerProvider.cs b/src/XUnit/src/Logging/XUnitLoggerProvider.cs deleted file mode 100644 index 1a58b2e..0000000 --- a/src/XUnit/src/Logging/XUnitLoggerProvider.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Microsoft.Extensions.Logging; - -namespace Fusonic.Extensions.XUnit.Logging; - -/// -/// Implementation of the Microsoft ILoggerProvider. Can be used for several services, for example for EF logs. -/// Logs to the ITestOutputHelper configured in the TestContext. -/// -public class XUnitLoggerProvider : ILoggerProvider -{ - public ILogger CreateLogger(string categoryName) => new XUnitLogger(categoryName); - - public void Dispose() => GC.SuppressFinalize(this); -} diff --git a/src/XUnit/src/TestContext.cs b/src/XUnit/src/TestContext.cs deleted file mode 100644 index bdc204a..0000000 --- a/src/XUnit/src/TestContext.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using System.Collections; -using System.Reflection; -using Xunit.Abstractions; - -namespace Fusonic.Extensions.XUnit; - -/// Provides a test context per method. -public static class TestContext -{ - private static readonly AsyncLocal InternalContext = new(); - internal static InternalTestContext? CurrentContext => InternalContext.Value; - - /// Gets the TestOutputHelper. There are also additional shortcuts to this with the WriteLine-Methods if you just want to log something to the test output. - public static ITestOutputHelper Out => CurrentContext!.OutputHelper; - - /// Gets the currently executing test method. - public static MethodInfo TestMethod => CurrentContext!.TestMethod; - - /// Gets the currently executing test class. - public static Type TestClass => CurrentContext!.TestClass; - - /// Allows you to put objects in the test context. - public static IDictionary Items => CurrentContext!.Items; - - /// Writes a message to the test output. - public static void WriteLine(string message) => Out.WriteLine(message); - - /// Writes a formatted message to the test output. - public static void WriteLine(string format, params object[] args) => Out.WriteLine(format, args); - - /// - /// Gets if the test context is set. Usually this is always true. You just might want to check it in framework code to verify that the test assembly uses - /// the FusonicTestFramework. - /// - public static bool IsSet => InternalContext.Value != null; - - internal static IDisposable Create(ITestOutputHelper testOutputHelper, MethodInfo testMethod, Type testClass) - { - InternalContext.Value = new InternalTestContext(testOutputHelper, testMethod, testClass); - - return InternalContext.Value; - } - - internal sealed class InternalTestContext : IDisposable - { - private IDictionary? items; - - public ITestOutputHelper OutputHelper { get; } - public MethodInfo TestMethod { get; } - public Type TestClass { get; } - public IDictionary Items => items ??= new Hashtable(); - - public InternalTestContext(ITestOutputHelper outputHelper, MethodInfo testMethod, Type testClass) - { - OutputHelper = outputHelper; - TestMethod = testMethod; - TestClass = testClass; - } - - public void Dispose() => InternalContext.Value = null; - } -} \ No newline at end of file diff --git a/src/XUnit/src/XUnit.csproj b/src/XUnit/src/XUnit.csproj deleted file mode 100644 index 7992d99..0000000 --- a/src/XUnit/src/XUnit.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - Fusonic.Extensions.XUnit - Fusonic.Extensions.XUnit - https://github.com/fusonic/dotnet-extensions - true - snupkg - Extensions to XUnit. Provides an own framework adding capabilities like a test context and attributes running before a test class gets instantiated. - false - bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml - - - - - - - \ No newline at end of file diff --git a/src/XUnit/test/BeforeAfterTestInvokeAttributeTests.cs b/src/XUnit/test/BeforeAfterTestInvokeAttributeTests.cs deleted file mode 100644 index bcafe2d..0000000 --- a/src/XUnit/test/BeforeAfterTestInvokeAttributeTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using System.Reflection; -using Fusonic.Extensions.XUnit.Framework; -using Xunit; - -namespace Fusonic.Extensions.XUnit.Tests; - -public class BeforeAfterTestInvokeAttributeTests -{ - [Test] - [Fact] - public void AttributeCalled() - { - Assert.True(TestAttribute.BeforeCalled); - Assert.False(TestAttribute.AfterCalled); //gets called after the test... - } - - public class TestAttribute : BeforeAfterTestInvokeAttribute - { - public static bool BeforeCalled { get; set; } - public static bool AfterCalled { get; set; } - - public override void Before(MethodInfo methodUnderTest) => BeforeCalled = true; - public override void After(MethodInfo methodUnderTest) => AfterCalled = true; - } -} \ No newline at end of file diff --git a/src/XUnit/test/Properties/AssemblyInfo.cs b/src/XUnit/test/Properties/AssemblyInfo.cs deleted file mode 100644 index 37d8be3..0000000 --- a/src/XUnit/test/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -[assembly: Fusonic.Extensions.XUnit.Framework.FusonicTestFramework] \ No newline at end of file diff --git a/src/XUnit/test/TestContextTests.cs b/src/XUnit/test/TestContextTests.cs deleted file mode 100644 index b56f549..0000000 --- a/src/XUnit/test/TestContextTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Fusonic GmbH. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for license information. - -using Xunit; - -namespace Fusonic.Extensions.XUnit.Tests; - -public class TestContextTests -{ - [Fact] - public void TestClassIsSet() - { - Assert.NotNull(TestContext.TestClass); - Assert.Equal(typeof(TestContextTests), TestContext.TestClass); - } - - [Fact] - public void TestMethodIsSet() - { - Assert.NotNull(TestContext.TestMethod); - Assert.Equal(nameof(TestMethodIsSet), TestContext.TestMethod.Name); - } - - [Fact] - public void TestOutputIsSet() - { - Assert.NotNull(TestContext.Out); - TestContext.WriteLine("Yep. It works."); - TestContext.WriteLine("{0}. It works {1}.", true, "too"); - } - - [Fact] - public void CanStoreItems() - { - TestContext.Items["2"] = 3; - - Assert.Equal(3, TestContext.Items["2"]); - Assert.Null(TestContext.Items["Something"]); - } -} diff --git a/src/XUnit/test/XUnit.Tests.csproj b/src/XUnit/test/XUnit.Tests.csproj deleted file mode 100644 index 41302e9..0000000 --- a/src/XUnit/test/XUnit.Tests.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - Fusonic.Extensions.XUnit.Tests - Fusonic.Extensions.XUnit.Tests - false - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - \ No newline at end of file