From bd43911d8d3ad059e01e4777f764504df5f17621 Mon Sep 17 00:00:00 2001 From: Adrien Hupond <10365998+ArwynFr@users.noreply.github.com> Date: Fri, 26 Jan 2024 15:51:21 +0100 Subject: [PATCH] ci: add continuous integration (#9) * ci: add continuous integration * test: add more tests * fix: dbcontext instance lifetime --- .config/dotnet-tools.json | 18 ++ .gitattributes | 1 + .github/CONTRIBUTING.md | 30 +++ .github/README.md | 94 ++++++++++ .github/USAGE.md | 175 ++++++++++++++++++ .github/workflows/integration.yaml | 72 ++++++- .gitignore | 3 + {.github => docs}/CONTRIBUTING-dod.adoc | 0 {.github => docs}/CONTRIBUTING-table.adoc | 0 {.github => docs}/CONTRIBUTING.adoc | 0 docs/Make.ps1 | 11 ++ {.github => docs}/README.adoc | 0 {.github => docs}/USAGE-basic.adoc | 0 {.github => docs}/USAGE-efcore.adoc | 0 {.github => docs}/USAGE.adoc | 0 src/runsettings.xml => runsettings.xml | 0 .../DummyDbContext.cs | 2 +- .../DummyException.Cs | 6 + .../DummyService.cs | 2 +- .../IDummyService.cs | 2 +- .../Program.cs | 1 + .../ConfigurationTests.cs | 2 +- .../DependencyTests.cs | 2 +- .../EmptyTest.cs | 2 +- .../DatabaseDefaultStrategyTests.cs | 31 ++++ .../DatabasePerTestStrategyTests.cs | 4 +- .../DatabaseTransactionStrategyTests.cs | 31 ++++ .../LoggingTests.cs | 17 +- .../ArwynFr.IntegrationTesting.csproj | 3 +- .../DatabaseTestStrategy.cs | 3 +- .../IDatabaseTestStrategy.cs | 2 +- .../IntegrationTestBase.Database.cs | 10 +- .../XUnitLoggerProvider.cs | 2 +- src/ArwynFr.IntegrationTesting/docs/README.md | 65 ------- 34 files changed, 500 insertions(+), 91 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/README.md create mode 100644 .github/USAGE.md rename {.github => docs}/CONTRIBUTING-dod.adoc (100%) rename {.github => docs}/CONTRIBUTING-table.adoc (100%) rename {.github => docs}/CONTRIBUTING.adoc (100%) create mode 100644 docs/Make.ps1 rename {.github => docs}/README.adoc (100%) rename {.github => docs}/USAGE-basic.adoc (100%) rename {.github => docs}/USAGE-efcore.adoc (100%) rename {.github => docs}/USAGE.adoc (100%) rename src/runsettings.xml => runsettings.xml (100%) create mode 100644 src/ArwynFr.IntegrationTesting.Tests.Target/DummyException.Cs create mode 100644 src/ArwynFr.IntegrationTesting.Tests/EntityFrameworkCore/DatabaseDefaultStrategyTests.cs create mode 100644 src/ArwynFr.IntegrationTesting.Tests/EntityFrameworkCore/DatabaseTransactionStrategyTests.cs delete mode 100644 src/ArwynFr.IntegrationTesting/docs/README.md diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 0fe55eb..48cd122 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -7,6 +7,24 @@ "commands": [ "dotnet-outdated" ] + }, + "dotnet-sonarscanner": { + "version": "6.0.0", + "commands": [ + "dotnet-sonarscanner" + ] + }, + "dotnet-format": { + "version": "5.1.250801", + "commands": [ + "dotnet-format" + ] + }, + "roslynator.dotnet.cli": { + "version": "0.8.3", + "commands": [ + "roslynator" + ] } } } \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..db2a596 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text eol=crlf \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..805f597 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,30 @@ +# Contributing guidelines + +This project welcomes contributions: + +**Request for support:** +TBD + +**Disclose vulnerability:** +Please [create a new security advisory on GitHub](https://github.com/ArwynFr/dotnet-integration-testing/security/advisories) + +**Report malfunctions:** +[Please create a new issue on GitHub](https://github.com/ArwynFr/dotnet-integration-testing/issues/new/choose) + +**Suggest a feature:** +[Please create a new issue on GitHub](https://github.com/ArwynFr/dotnet-integration-testing/issues/new/choose) + +**Offer some code:** +Please [fork the repository](https://github.com/ArwynFr/dotnet-integration-testing/fork) +and [submit a pull-request](https://github.com/ArwynFr/dotnet-integration-testing/compare) + +## Definition of Done + +Merging a pull request requires: + +- dotnet format passes +- dotnet roslynator analyze passes +- dotnet oudated passes +- markdownlint passes +- SonarCloud QualityGate passes +- Documentation updated diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..1b3527c --- /dev/null +++ b/.github/README.md @@ -0,0 +1,94 @@ +# ArwynFr.IntegrationTesting + +This library provides utility classes for writing integration tests in +dotnet using `XUnit` and `WebApplicationFactory`. + +![Nuget.org](https://img.shields.io/nuget/v/ArwynFr.IntegrationTesting?style=for-the-badge) +![Nuget.org](https://img.shields.io/nuget/dt/ArwynFr.IntegrationTesting?style=for-the-badge) +![GitHub +License](https://img.shields.io/github/license/ArwynFr/dotnet-integration-testing?style=for-the-badge) + +## Installation + + dotnet add package ArwynFr.IntegrationTesting + +## Usage + +Read [advanced usage +documentation](https://github.com/ArwynFr/dotnet-integration-testing/blob/main/.github/USAGE.adoc) +for further details. + +By default, the lib redirects the tested application logs to XUnit +output, so you get them in the test output in case of failure. It also +overwrites the application configuration with values from user secrets +and environement variables. + + public class MyTest : IntegrationTestBase + { + public MyTest(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task OnTest() + { + // Call system under test + var response = await Client.GetFromJsonAsync($"/order"); + + response.Should().HaveValue(); + } + + // Override a service with fake implementation in the tested app + protected override void ConfigureAppServices(IServiceCollection services) + => services.AddSingleton(); + } + +### EntityFrameworkCore integration + + public class TestBaseDb : IntegrationTestBase + { + public TestBaseDb(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task OnTest() + { + // Access the injected dbcontext + var value = await Database.Values + .Where(val => val.Id == 1) + .Select(val => val.Result) + .FirstOrDefaultAsync(); + + // Call system under test + var result = await Client.GetFromJsonAsync("/api/value/1"); + + result.Should().Be(value + 1); + } + + // Create and drop a database for every test execution + protected override IDatabaseTestStrategy DatabaseTestStrategy + => IDatabaseTestStrategy.DatabasePerTest; + + // Configure EFcore with a random database name + protected override void ConfigureDbContext(DbContextOptionsBuilder builder) + => builder.UseSqlite($"Data Source={Guid.NewGuid()}.sqlite"); + } + +## Contributing + +This project welcomes contributions: + +**Request for support:** +TBD + +**Disclose vulnerability:** +Please [create a new security advisory on GitHub](https://github.com/ArwynFr/dotnet-integration-testing/security/advisories) + +**Report malfunctions:** +[Please create a new issue on GitHub](https://github.com/ArwynFr/dotnet-integration-testing/issues/new/choose) + +**Suggest a feature:** +[Please create a new issue on GitHub](https://github.com/ArwynFr/dotnet-integration-testing/issues/new/choose) + +**Offer some code:** +Please [fork the repository](https://github.com/ArwynFr/dotnet-integration-testing/fork) +and [submit a pull-request](https://github.com/ArwynFr/dotnet-integration-testing/compare) +\ +[Read our definition of done in contributing guidelines](https://github.com/ArwynFr/dotnet-integration-testing/blob/main/.github/CONTRIBUTING.md) diff --git a/.github/USAGE.md b/.github/USAGE.md new file mode 100644 index 0000000..4bda996 --- /dev/null +++ b/.github/USAGE.md @@ -0,0 +1,175 @@ +# Advanced usage documentation + +This library uses `WebApplicationFactory` for integration testing. +Please read [Microsoft’s paper on integration +testing](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-8.0) +for details on how this library works. + +## Definitions + +**Tested application** +A dotnet application that you want to test. Also known as the *system +under test* (SUT). + +**Entrypoint** +A specific class of the tested application that is run when the +application start. For top-level statement applications this is the +`Program` class. + +**Test project** +A XUnit test project that implements tests and ensures that the tested +application behaves as expected. + +## Basic integration test + +Simply extend the `IntegrationTestBase` class and provide +the entrypoint class you want to test: + + public class MyTest : IntegrationTestBase + { + public MyTest(ITestOutputHelper output) : base(output) { } + } + +## Arranging the tested application + +You can override the following methods to customize the tested +application: + +ConfigureAppConfiguration +Change the sources for the app configuration. By default, overrides the +application configuration sources with the following (lower overrides +higher): + +- `appsettings.json` file +- User secrets associated with the assembly that contains the entrypoint +- User secrets associated with the assembly that contains the test class +- Environment variables + +ConfigureAppLogging +Change the app logging configuration. By default, redirects all logs +(all namespaces and all levels) to the XUnit output helper. If the test +fails, you get all the tested application logs in the test output. + +ConfigureAppServices +Override dependency injection services of the tested application. + +If your test code and the tested application need to access the same +instance of a service, you need to inject this instance using a +Singleton lifetime. Accessing the DI container outside of the client +call will return an unscoped provider as scopes are created and disposed +by the framework for each request. + + // Override a service with custom implementation in the tested app + protected override void ConfigureAppServices(IServiceCollection services) + => services.AddSingleton(); + + [Fact] + public async Task OnTest() + { + var expected = 3; + + // Access the injected service from the test code + var service = Services.GetRequiredService(); + service.SetValue(expected); + + var response = await Client.GetFromJsonAsync($"/value"); + + response.Should().Be(expected); + } + +## Accessing the tested application + +Client +You can access the tested application using the `Client` property which +returns a pseudo `HttpClient` created by `WebApplicationFactory`. You +access your application like a client application would: + + + + [Fact] + public async Task OnTest() + { + var response = await Client.GetFromJsonAsync($"/order"); + response.Should().HaveValue(); + } + +Configuration +This property grants you acces to the `IConfiguration` values that are +currently available to the tested application. + +Services +This property grants you access to the DI service provider of the tested +application. + +If your test code and the tested application need to access the same +instance of a service, you need to inject this instance using a +Singleton lifetime. Accessing the DI container outside of the client +call will return an unscoped provider as scopes are created and disposed +by the framework for each request. + +## Extending the behavior of the test class + +You can override the following methods to run code before and after each +test: + +InitializeAsync +Code executed before the execution of each test of the class. + +DisposeAsync +Code executed after the execution of each test of the class. + +## EntityFrameworkCore integration + +The library provides specific support for efcore. You can achieve this +integration by extending the +`IntegrationTestBase` class instead of the one +that only uses the entrypoint. + +You will need to override the abstract `ConfigureDbContext` method to +tell the dependency injection library how to configure your context. A +context instance will be generated per test and injected in your target +app as a singleton. You can access the same context instance in your +test through the `Database` property. + + protected override void ConfigureDbContext(DbContextOptionsBuilder builder) + => builder.UseSqlite($"Data Source=test.sqlite"); + +The base class also exposes a `DatabaseTestStrategy` property that +allows you to customize the test behavior regarding the database. You +can write your own implementation which requires you to write specific +code that will run before and after each test to set your database. + +The library comes with 3 standard behaviors: + +`IDatabaseTestStrategy.Default` +By default the library simply instantates the context and disposes the +instance after the test execution. + +`IDatabaseTestStrategy.Transaction` +This behavior will execute each test in a separate transaction. This can +be used to prevent write operations to change the contents of the +database. Obviously requires a database engine that supports +transactions. + +`IDatabaseTestStrategy.DatabasePerTest` +This behavior creates a fresh database before test execution and drops +it afterwards. It also applies migrations if any are found, otherwise it +will use `EnsureCreated` (read [Create and Drop +APIs](https://learn.microsoft.com/en-us/ef/core/managing-schemas/ensure-created) +to understand how this might affect your test results). This allows test +parallelization when transaction isolation is not sufficient or +unavailable. You must combine this behavior with a random name +interpolation in the connection string to run each test on it’s own +database in parallel. Otherwise the tests will try to access the same +database concurrently and will fail to drop it while other tests are +running: + + + + protected override IDatabaseTestStrategy DatabaseTestStrategy + => IDatabaseTestStrategy.DatabasePerTest; + + protected override void ConfigureDbContext(DbContextOptionsBuilder builder) + => builder.UseSqlite($"Data Source={Guid.NewGuid()}.sqlite"); + +This beahvior WILL drop your database after each test ! diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index 4e5c475..56bb6fd 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -7,20 +7,80 @@ on: push: branches: [main] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - integration: + sonarqube: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Use Java 21 Temuerin + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "21" + - name: Restore dotnet tools + run: dotnet tool restore + - name: SonarCloud configuration + run: > + dotnet sonarscanner begin + /o:arwynfr + /key:ArwynFr_dotnet-integration-testing + /d:sonar.host.url=https://sonarcloud.io + /d:sonar.cs.opencover.reportsPaths=TestResults/*/coverage.opencover.xml + /d:sonar.verbose=true + env: + SONAR_TOKEN: ${{ secrets.sonar_token }} + - name: Run automated tests + run: dotnet test src/ArwynFr.IntegrationTesting.Tests --settings runsettings.xml --results-directory TestResults + - name: SonarCloud analyze + run: dotnet sonarscanner end + env: + SONAR_TOKEN: ${{ secrets.sonar_token }} + + code-lint: runs-on: ubuntu-latest defaults: run: working-directory: src steps: - - uses: actions/checkout@v4 - + - name: Checkout sources + uses: actions/checkout@v4 - name: Restore dotnet tools run: dotnet tool restore + - name: Check dotnet-format + run: dotnet format --verify-no-changes + - name: Check roslynator + run: dotnet roslynator analyze - - name: Test for outdated dependencies + docs-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Check markdown + uses: nosborn/github-action-markdown-cli@v3.3.0 + with: + files: . + dot: true + + not-outdated: + runs-on: ubuntu-latest + defaults: + run: + working-directory: src + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Restore dotnet tools + run: dotnet tool restore + - name: Check outdated nuget packages run: dotnet outdated --fail-on-updates - - name: Run automated tests - run: dotnet test ArwynFr.IntegrationTesting.Tests --settings runsettings.xml --results-directory TestResults + # - name: Continuous integration + # run: gh pr merge --squash --auto + # env: + # GH_TOKEN: ${{ github.token }} diff --git a/.gitignore b/.gitignore index 104b544..7763585 100644 --- a/.gitignore +++ b/.gitignore @@ -482,3 +482,6 @@ $RECYCLE.BIN/ # Vim temporary swap files *.swp + +# Sonarqube +.sonarqube \ No newline at end of file diff --git a/.github/CONTRIBUTING-dod.adoc b/docs/CONTRIBUTING-dod.adoc similarity index 100% rename from .github/CONTRIBUTING-dod.adoc rename to docs/CONTRIBUTING-dod.adoc diff --git a/.github/CONTRIBUTING-table.adoc b/docs/CONTRIBUTING-table.adoc similarity index 100% rename from .github/CONTRIBUTING-table.adoc rename to docs/CONTRIBUTING-table.adoc diff --git a/.github/CONTRIBUTING.adoc b/docs/CONTRIBUTING.adoc similarity index 100% rename from .github/CONTRIBUTING.adoc rename to docs/CONTRIBUTING.adoc diff --git a/docs/Make.ps1 b/docs/Make.ps1 new file mode 100644 index 0000000..64ea6df --- /dev/null +++ b/docs/Make.ps1 @@ -0,0 +1,11 @@ +$DocsDirectory = Convert-Path $PSScriptRoot +$GithubDirectory = Join-Path $PSScriptRoot ../.github + +'README', 'USAGE', 'CONTRIBUTING' | ForEach-Object { + $source = "${DocsDirectory}/${_}.adoc" + $docbook = "${DocsDirectory}/${_}.xml" + $target = "${GithubDirectory}/${_}.md" + & asciidoctor.bat -b docbook $source -o $docbook + & pandoc.exe -f docbook -t markdown_strict $docbook -o $target + Remove-Item $docbook +} \ No newline at end of file diff --git a/.github/README.adoc b/docs/README.adoc similarity index 100% rename from .github/README.adoc rename to docs/README.adoc diff --git a/.github/USAGE-basic.adoc b/docs/USAGE-basic.adoc similarity index 100% rename from .github/USAGE-basic.adoc rename to docs/USAGE-basic.adoc diff --git a/.github/USAGE-efcore.adoc b/docs/USAGE-efcore.adoc similarity index 100% rename from .github/USAGE-efcore.adoc rename to docs/USAGE-efcore.adoc diff --git a/.github/USAGE.adoc b/docs/USAGE.adoc similarity index 100% rename from .github/USAGE.adoc rename to docs/USAGE.adoc diff --git a/src/runsettings.xml b/runsettings.xml similarity index 100% rename from src/runsettings.xml rename to runsettings.xml diff --git a/src/ArwynFr.IntegrationTesting.Tests.Target/DummyDbContext.cs b/src/ArwynFr.IntegrationTesting.Tests.Target/DummyDbContext.cs index 13861b5..6587e62 100644 --- a/src/ArwynFr.IntegrationTesting.Tests.Target/DummyDbContext.cs +++ b/src/ArwynFr.IntegrationTesting.Tests.Target/DummyDbContext.cs @@ -10,4 +10,4 @@ public class DummyDbContext : DbContext protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly); -} +} \ No newline at end of file diff --git a/src/ArwynFr.IntegrationTesting.Tests.Target/DummyException.Cs b/src/ArwynFr.IntegrationTesting.Tests.Target/DummyException.Cs new file mode 100644 index 0000000..909e706 --- /dev/null +++ b/src/ArwynFr.IntegrationTesting.Tests.Target/DummyException.Cs @@ -0,0 +1,6 @@ +namespace ArwynFr.IntegrationTesting.Tests.Target; + +public class DummyException : InvalidOperationException +{ + +} \ No newline at end of file diff --git a/src/ArwynFr.IntegrationTesting.Tests.Target/DummyService.cs b/src/ArwynFr.IntegrationTesting.Tests.Target/DummyService.cs index 7949bb9..d72153e 100644 --- a/src/ArwynFr.IntegrationTesting.Tests.Target/DummyService.cs +++ b/src/ArwynFr.IntegrationTesting.Tests.Target/DummyService.cs @@ -1,3 +1,3 @@ namespace ArwynFr.IntegrationTesting.Tests.Target; -public class DummyService : IDummyService; +public class DummyService : IDummyService; \ No newline at end of file diff --git a/src/ArwynFr.IntegrationTesting.Tests.Target/IDummyService.cs b/src/ArwynFr.IntegrationTesting.Tests.Target/IDummyService.cs index cf1fe4f..59578b7 100644 --- a/src/ArwynFr.IntegrationTesting.Tests.Target/IDummyService.cs +++ b/src/ArwynFr.IntegrationTesting.Tests.Target/IDummyService.cs @@ -1,3 +1,3 @@ namespace ArwynFr.IntegrationTesting.Tests.Target; -public interface IDummyService; +public interface IDummyService; \ No newline at end of file diff --git a/src/ArwynFr.IntegrationTesting.Tests.Target/Program.cs b/src/ArwynFr.IntegrationTesting.Tests.Target/Program.cs index f4d89aa..3d6a9ba 100644 --- a/src/ArwynFr.IntegrationTesting.Tests.Target/Program.cs +++ b/src/ArwynFr.IntegrationTesting.Tests.Target/Program.cs @@ -5,6 +5,7 @@ builder.Services.AddSqlite("invalid"); var app = builder.Build(); app.MapGet("/", () => "Hello World!"); +app.MapGet("/error", () => { throw new DummyException(); }); app.MapGet("/service", (IDummyService service) => service.GetHashCode()); app.Run(); diff --git a/src/ArwynFr.IntegrationTesting.Tests/ConfigurationTests.cs b/src/ArwynFr.IntegrationTesting.Tests/ConfigurationTests.cs index b71254a..ca23e06 100644 --- a/src/ArwynFr.IntegrationTesting.Tests/ConfigurationTests.cs +++ b/src/ArwynFr.IntegrationTesting.Tests/ConfigurationTests.cs @@ -6,4 +6,4 @@ public class ConfigurationTests(ITestOutputHelper output) : IntegrationTestBase< { // WARNING : configuration overrides are not unit testable // it would require to programatically set environment varaibles or user secrets before class instanciation -} +} \ No newline at end of file diff --git a/src/ArwynFr.IntegrationTesting.Tests/DependencyTests.cs b/src/ArwynFr.IntegrationTesting.Tests/DependencyTests.cs index 35f7962..5670383 100644 --- a/src/ArwynFr.IntegrationTesting.Tests/DependencyTests.cs +++ b/src/ArwynFr.IntegrationTesting.Tests/DependencyTests.cs @@ -31,4 +31,4 @@ protected override void ConfigureAppServices(IServiceCollection services) => services.AddSingleton(); private class OverrideDummyService : IDummyService; -} +} \ No newline at end of file diff --git a/src/ArwynFr.IntegrationTesting.Tests/EmptyTest.cs b/src/ArwynFr.IntegrationTesting.Tests/EmptyTest.cs index a1a58df..8b33fda 100644 --- a/src/ArwynFr.IntegrationTesting.Tests/EmptyTest.cs +++ b/src/ArwynFr.IntegrationTesting.Tests/EmptyTest.cs @@ -9,4 +9,4 @@ public class EmptyTests(ITestOutputHelper output) : IntegrationTestBase { [Fact] public void EmptyTest_ShouldRunWithoutError() => Expression.Empty(); -} +} \ No newline at end of file diff --git a/src/ArwynFr.IntegrationTesting.Tests/EntityFrameworkCore/DatabaseDefaultStrategyTests.cs b/src/ArwynFr.IntegrationTesting.Tests/EntityFrameworkCore/DatabaseDefaultStrategyTests.cs new file mode 100644 index 0000000..a76f070 --- /dev/null +++ b/src/ArwynFr.IntegrationTesting.Tests/EntityFrameworkCore/DatabaseDefaultStrategyTests.cs @@ -0,0 +1,31 @@ +using ArwynFr.IntegrationTesting.Tests.Target; + +using FluentAssertions; + +using Microsoft.EntityFrameworkCore; + +using Xunit; +using Xunit.Abstractions; + +namespace ArwynFr.IntegrationTesting.Tests.EntityFrameworkCore; + +public class DatabaseDefaultStrategyTests(ITestOutputHelper output) : IntegrationTestBase(output) +{ + [Fact] + public void TestShouldCreateDatabase() + { + Database.Database.GetDbConnection().Should().NotBeNull(); + } + + public override async Task InitializeAsync() + { + await Database.Database.EnsureCreatedAsync(); + await base.InitializeAsync(); + } + + protected override IDatabaseTestStrategy DatabaseTestStrategy + => IDatabaseTestStrategy.Default; + + protected override void ConfigureDbContext(DbContextOptionsBuilder builder) + => builder.UseSqlite($@"Data Source={Guid.NewGuid()}.sqlite"); +} \ No newline at end of file diff --git a/src/ArwynFr.IntegrationTesting.Tests/EntityFrameworkCore/DatabasePerTestStrategyTests.cs b/src/ArwynFr.IntegrationTesting.Tests/EntityFrameworkCore/DatabasePerTestStrategyTests.cs index 16be092..3cabe3f 100644 --- a/src/ArwynFr.IntegrationTesting.Tests/EntityFrameworkCore/DatabasePerTestStrategyTests.cs +++ b/src/ArwynFr.IntegrationTesting.Tests/EntityFrameworkCore/DatabasePerTestStrategyTests.cs @@ -12,7 +12,7 @@ namespace ArwynFr.IntegrationTesting.Tests.EntityFrameworkCore; public class DatabasePerTestStrategyTests(ITestOutputHelper output) : IntegrationTestBase(output) { [Fact] - public void TestShouldExecuteInTransaction() + public void TestShouldCreateDatabase() { Database.Database.GetDbConnection().Should().NotBeNull(); } @@ -22,4 +22,4 @@ protected override IDatabaseTestStrategy DatabaseTestStrategy protected override void ConfigureDbContext(DbContextOptionsBuilder builder) => builder.UseSqlite($@"Data Source={Guid.NewGuid()}.sqlite"); -} +} \ No newline at end of file diff --git a/src/ArwynFr.IntegrationTesting.Tests/EntityFrameworkCore/DatabaseTransactionStrategyTests.cs b/src/ArwynFr.IntegrationTesting.Tests/EntityFrameworkCore/DatabaseTransactionStrategyTests.cs new file mode 100644 index 0000000..a1ec9ca --- /dev/null +++ b/src/ArwynFr.IntegrationTesting.Tests/EntityFrameworkCore/DatabaseTransactionStrategyTests.cs @@ -0,0 +1,31 @@ +using ArwynFr.IntegrationTesting.Tests.Target; + +using FluentAssertions; + +using Microsoft.EntityFrameworkCore; + +using Xunit; +using Xunit.Abstractions; + +namespace ArwynFr.IntegrationTesting.Tests.EntityFrameworkCore; + +public class DatabaseTransactionStrategyTests(ITestOutputHelper output) : IntegrationTestBase(output) +{ + [Fact] + public void TestShouldExecuteInTransaction() + { + Database.Database.CurrentTransaction.Should().NotBeNull(); + } + + public override async Task InitializeAsync() + { + await Database.Database.EnsureCreatedAsync(); + await base.InitializeAsync(); + } + + protected override IDatabaseTestStrategy DatabaseTestStrategy + => IDatabaseTestStrategy.Transaction; + + protected override void ConfigureDbContext(DbContextOptionsBuilder builder) + => builder.UseSqlite($@"Data Source={Guid.NewGuid()}.sqlite"); +} \ No newline at end of file diff --git a/src/ArwynFr.IntegrationTesting.Tests/LoggingTests.cs b/src/ArwynFr.IntegrationTesting.Tests/LoggingTests.cs index ca4df36..5c0fb88 100644 --- a/src/ArwynFr.IntegrationTesting.Tests/LoggingTests.cs +++ b/src/ArwynFr.IntegrationTesting.Tests/LoggingTests.cs @@ -1,4 +1,6 @@ -using FluentAssertions; +using ArwynFr.IntegrationTesting.Tests.Target; + +using FluentAssertions; using Xunit; using Xunit.Abstractions; @@ -19,6 +21,15 @@ public async Task EmptyTest_ShouldWriteToXunit() { await Client.GetAsync("/"); output.Should().BeOfType() - .Subject.Output.Should().NotBeNullOrEmpty(); + .Subject.Output.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task ExceptionThrown_ShouldWriteToXunit() + { + var response = await Client.GetAsync("/error"); + response.Should().HaveServerError(); + output.Should().BeOfType() + .Subject.Output.Should().Contain(nameof(DummyException)); } -} +} \ No newline at end of file diff --git a/src/ArwynFr.IntegrationTesting/ArwynFr.IntegrationTesting.csproj b/src/ArwynFr.IntegrationTesting/ArwynFr.IntegrationTesting.csproj index 4fcb2f9..dbebf08 100644 --- a/src/ArwynFr.IntegrationTesting/ArwynFr.IntegrationTesting.csproj +++ b/src/ArwynFr.IntegrationTesting/ArwynFr.IntegrationTesting.csproj @@ -11,6 +11,7 @@ Copyright (c) ArwynFr 2024 README.md MIT + false @@ -23,7 +24,7 @@ - + diff --git a/src/ArwynFr.IntegrationTesting/DatabaseTestStrategy.cs b/src/ArwynFr.IntegrationTesting/DatabaseTestStrategy.cs index c94fffc..f507a59 100644 --- a/src/ArwynFr.IntegrationTesting/DatabaseTestStrategy.cs +++ b/src/ArwynFr.IntegrationTesting/DatabaseTestStrategy.cs @@ -38,7 +38,8 @@ public DatabaseTestStrategy WithTransaction() transaction = true; return this; } - private Task UpdateDatabase(TContext context) + + private static Task UpdateDatabase(TContext context) => context.Database.GetMigrations().Any() ? context.Database.MigrateAsync() : context.Database.EnsureCreatedAsync(); diff --git a/src/ArwynFr.IntegrationTesting/IDatabaseTestStrategy.cs b/src/ArwynFr.IntegrationTesting/IDatabaseTestStrategy.cs index b72f12d..bb1e47b 100644 --- a/src/ArwynFr.IntegrationTesting/IDatabaseTestStrategy.cs +++ b/src/ArwynFr.IntegrationTesting/IDatabaseTestStrategy.cs @@ -24,4 +24,4 @@ public interface IDatabaseTestStrategy Task DisposeAsync(TContext database); Task InitializeAsync(TContext database); -} +} \ No newline at end of file diff --git a/src/ArwynFr.IntegrationTesting/IntegrationTestBase.Database.cs b/src/ArwynFr.IntegrationTesting/IntegrationTestBase.Database.cs index 73c89a4..45c61ad 100644 --- a/src/ArwynFr.IntegrationTesting/IntegrationTestBase.Database.cs +++ b/src/ArwynFr.IntegrationTesting/IntegrationTestBase.Database.cs @@ -13,9 +13,7 @@ public abstract class IntegrationTestBase : IntegrationTestB { protected IntegrationTestBase(ITestOutputHelper output) : base(output) => Expression.Empty(); - protected TContext Database => new ServiceCollection() - .AddDbContext(ConfigureDbContext, ServiceLifetime.Singleton) - .BuildServiceProvider().GetRequiredService(); + protected TContext Database => Services.GetRequiredService(); protected virtual IDatabaseTestStrategy DatabaseTestStrategy => IDatabaseTestStrategy.Default; @@ -23,8 +21,10 @@ public abstract class IntegrationTestBase : IntegrationTestB public override Task InitializeAsync() => DatabaseTestStrategy.InitializeAsync(Database); - protected override void ConfigureAppServices(IServiceCollection services) => services.AddSingleton(Database); + protected override void ConfigureAppServices(IServiceCollection services) => services.AddSingleton(new ServiceCollection() + .AddDbContext(ConfigureDbContext, ServiceLifetime.Singleton) + .BuildServiceProvider().GetRequiredService()); protected abstract void ConfigureDbContext(DbContextOptionsBuilder builder); -} +} \ No newline at end of file diff --git a/src/ArwynFr.IntegrationTesting/XUnitLoggerProvider.cs b/src/ArwynFr.IntegrationTesting/XUnitLoggerProvider.cs index b99c148..6c2a841 100644 --- a/src/ArwynFr.IntegrationTesting/XUnitLoggerProvider.cs +++ b/src/ArwynFr.IntegrationTesting/XUnitLoggerProvider.cs @@ -15,4 +15,4 @@ internal sealed class XUnitLoggerProvider : ILoggerProvider public ILogger CreateLogger(string categoryName) => new XunitLogger(categoryName, output); public void Dispose() => Expression.Empty(); -} +} \ No newline at end of file diff --git a/src/ArwynFr.IntegrationTesting/docs/README.md b/src/ArwynFr.IntegrationTesting/docs/README.md deleted file mode 100644 index 722e253..0000000 --- a/src/ArwynFr.IntegrationTesting/docs/README.md +++ /dev/null @@ -1,65 +0,0 @@ -# ArwynFr.IntegrationTesting - -This library provides utility classes for writing integration tests in dotnet using `XUnit` and `WebApplicationFactory`. - -## Installing - -``` -> dotnet add package ArwynFr.IntegrationTesting -``` - -## Usage - -See [advanced usage guide](USAGE.adoc) for details. - -By default, the lib redirects the tested application logs to XUnit output, so you get them in the test output in case of failure. It also overwrites the application configuration with values from user secrets and environement variables. - -```cs -public class MyTest : IntegrationTestBase -{ - public MyTest(ITestOutputHelper output) : base(output) { } - - [Fact] - public async Task OnTest() - { - var response = await Client.GetFromJsonAsync($"/order"); - - response.Should().HaveValue(); - } - - // Override a service with fake implementation in the tested app - protected override void ConfigureAppServices(IServiceCollection services) - => services.AddSingleton(); -} -``` - -## EntityFrameworkCore integration - -```cs -public class TestBaseDb : IntegrationTestBase -{ - public TestBaseDb(ITestOutputHelper output) : base(output) { } - - [Fact] - public async Task OnTest() - { - // Access the injected dbcontext - var value = await Database.Values - .Where(val => val.Id = 1) - .Select(val => val.Result) - .FirstOrDefaultAsync(); - - var result = await Client.GetFromJsonAsync("/api/value/1"); - - result.Should().Be(value + 1); - } - - // Create and drop a database for every test execution - protected override IDatabaseTestStrategy DatabaseTestStrategy - => IDatabaseTestStrategy.DatabasePerTest; - - // Configure EFcore with a random database name - protected override void ConfigureDbContext(DbContextOptionsBuilder builder) - => builder.UseSqlite($"Data Source={Guid.NewGuid()}.sqlite"); -} -``` \ No newline at end of file