diff --git a/.docfx/api/namespaces/Codebelt.Extensions.Xunit.Hosting.AspNetCore.md b/.docfx/api/namespaces/Codebelt.Extensions.Xunit.Hosting.AspNetCore.md index a224477..7327a8e 100644 --- a/.docfx/api/namespaces/Codebelt.Extensions.Xunit.Hosting.AspNetCore.md +++ b/.docfx/api/namespaces/Codebelt.Extensions.Xunit.Hosting.AspNetCore.md @@ -6,11 +6,25 @@ The `Codebelt.Extensions.Xunit.Hosting.AspNetCore` namespace contains types that [!INCLUDE [availability-modern](../../includes/availability-modern.md)] -Complements: [Microsoft.AspNetCore.TestHost namespace](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.testhost) 🔗 - -### Extension Methods - -|Type|Ext|Methods| -|--:|:-:|---| +Complements: [Microsoft.AspNetCore.TestHost namespace](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.testhost) 🔗 + +### Fixture Naming Convention + +ASP.NET Core host fixtures follow the same lifecycle naming convention as the hosting package: + +|Prefix|Convention| +|---|---| +|`Managed`|The fixture owns host creation, configuration, startup and disposal using the default host runner.| +|`SelfManaged`|The fixture owns host creation and configuration, but leaves host startup to the test.| +|`BlockingManaged`|The fixture owns the host lifecycle and starts the host synchronously before returning control to the test.| + +Application-entry-point fixtures use the `BlockingManaged` prefix by default. ASP.NET Core application tests expose a `TestServer`, and callers should receive a started server after fixture initialization. Use `BlockingManagedWebApplicationFixture` when testing an existing ASP.NET Core application entry point with `TestServer`. + +`BlockingManagedWebHostFixture` remains the opt-in blocking variant for the lower-level web host fixture family. The application-entry-point fixture is named `BlockingManagedWebApplicationFixture` directly because this API is blocking by convention from its first release. + +### Extension Methods + +|Type|Ext|Methods| +|--:|:-:|---| |HttpClient|⬇️|`ToHttpResponseMessageAsync`| |IServiceCollection|⬇️|`AddFakeHttpContextAccessor`| diff --git a/.docfx/api/namespaces/Codebelt.Extensions.Xunit.Hosting.md b/.docfx/api/namespaces/Codebelt.Extensions.Xunit.Hosting.md index 7b48a6b..a588471 100644 --- a/.docfx/api/namespaces/Codebelt.Extensions.Xunit.Hosting.md +++ b/.docfx/api/namespaces/Codebelt.Extensions.Xunit.Hosting.md @@ -6,12 +6,24 @@ The `Codebelt.Extensions.Xunit.Hosting` namespace contains types that provides a [!INCLUDE [availability-default](../../includes/availability-default.md)] -Complements: [xUnit: Shared Context between Tests](https://xunit.net/docs/shared-context) 🔗 - -### Extension Methods - -|Type|Ext|Methods| -|--:|:-:|---| +Complements: [xUnit: Shared Context between Tests](https://xunit.net/docs/shared-context) 🔗 + +### Fixture Naming Convention + +Host fixtures follow a lifecycle naming convention: + +|Prefix|Convention| +|---|---| +|`Managed`|The fixture owns host creation, configuration, startup and disposal using the default host runner.| +|`SelfManaged`|The fixture owns host creation and configuration, but leaves host startup to the test.| +|`BlockingManaged`|The fixture owns the host lifecycle and starts the host synchronously before returning control to the test.| + +Application-entry-point fixtures use the `BlockingManaged` prefix by default. Existing application entry points are discovered and built from their `Program` assembly, so tests should receive a ready host after fixture initialization. Use `BlockingManagedApplicationFixture` when testing console, worker or generic host applications from an existing entry point. + +### Extension Methods + +|Type|Ext|Methods| +|--:|:-:|---| |ILogger{T}|⬇️|`GetTestStore`| |IServiceCollection|⬇️|`AddXunitTestOutputHelperAccessor`| |IServiceProvider|⬇️|`GetRequiredScopedService`| diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index b84555d..d1cd6b4 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -12,6 +12,10 @@ on: options: - Debug - Release + run_mac_tests: + type: boolean + description: Run the macOS test matrix despite the additional cost and runtime. + default: false permissions: contents: read @@ -21,6 +25,7 @@ jobs: name: initialize runs-on: ubuntu-24.04 outputs: + run-mac-tests: ${{ steps.vars.outputs.run-mac-tests }} run-privileged-jobs: ${{ steps.vars.outputs.run-privileged-jobs }} strong-name-key-filename: ${{ steps.vars.outputs.strong-name-key-filename }} build-switches: ${{ steps.vars.outputs.build-switches }} @@ -29,6 +34,12 @@ jobs: name: calculate workflow variables shell: bash run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.run_mac_tests }}" == "true" ]]; then + echo "run-mac-tests=true" >> "$GITHUB_OUTPUT" + else + echo "run-mac-tests=false" >> "$GITHUB_OUTPUT" + fi + if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then echo "run-privileged-jobs=false" >> "$GITHUB_OUTPUT" echo "strong-name-key-filename=" >> "$GITHUB_OUTPUT" @@ -101,10 +112,72 @@ jobs: build: true # we need to build for .net48 tests download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }} + test_mac: + if: ${{ needs.init.outputs.run-mac-tests == 'true' }} + name: call-test-mac + needs: [init, build] + strategy: + fail-fast: false + matrix: + arch: [X64, ARM64] + configuration: [Debug, Release] + uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 + with: + runs-on: ${{ matrix.arch == 'ARM64' && 'macos-26' || 'macos-26-intel' }} + configuration: ${{ matrix.configuration }} + build-switches: -p:SkipSignAssembly=true + restore: true + build: true # required for xunitv3 + download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }} + + test_qualitygate: + if: ${{ always() }} + name: test-qualitygate + needs: [init, test_linux, test_windows, test_mac] + runs-on: ubuntu-24.04 + steps: + - name: Evaluate test results + shell: bash + env: + RUN_MAC_TESTS: ${{ needs.init.outputs.run-mac-tests }} + TEST_LINUX_RESULT: ${{ needs.test_linux.result }} + TEST_WINDOWS_RESULT: ${{ needs.test_windows.result }} + TEST_MAC_RESULT: ${{ needs.test_mac.result }} + run: | + require_success() { + local job_name="$1" + local job_result="$2" + + if [[ "$job_result" != "success" ]]; then + echo "::error::$job_name finished with '$job_result'." + exit 1 + fi + } + + require_success_or_skip() { + local job_name="$1" + local job_enabled="$2" + local job_result="$3" + + if [[ "$job_enabled" == "true" ]]; then + require_success "$job_name" "$job_result" + return + fi + + if [[ "$job_result" != "success" && "$job_result" != "skipped" ]]; then + echo "::error::$job_name finished with '$job_result' while disabled." + exit 1 + fi + } + + require_success "test_linux" "$TEST_LINUX_RESULT" + require_success "test_windows" "$TEST_WINDOWS_RESULT" + require_success_or_skip "test_mac" "$RUN_MAC_TESTS" "$TEST_MAC_RESULT" + sonarcloud: - if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }} + if: ${{always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success'}} name: call-sonarcloud - needs: [init, build, test_linux, test_windows] + needs: [init, build, test_qualitygate] uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3 with: organization: geekle @@ -113,18 +186,18 @@ jobs: secrets: inherit codecov: - if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }} + if: ${{always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success'}} name: call-codecov - needs: [init, build, test_linux, test_windows] + needs: [init, build, test_qualitygate] uses: codebeltnet/jobs-codecov/.github/workflows/default.yml@v1 with: repository: codebeltnet/xunit secrets: inherit codeql: - if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }} + if: ${{always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success'}} name: call-codeql - needs: [init, build, test_linux, test_windows] + needs: [init, build, test_qualitygate] uses: codebeltnet/jobs-codeql/.github/workflows/default.yml@v3 permissions: security-events: write @@ -132,7 +205,7 @@ jobs: deploy: if: github.event_name != 'pull_request' name: call-nuget - needs: [build, pack, test_linux, test_windows, sonarcloud, codecov, codeql] + needs: [build, pack, test_qualitygate, sonarcloud, codecov, codeql] uses: codebeltnet/jobs-nuget-push/.github/workflows/default.yml@v3 with: version: ${{ needs.build.outputs.version }} diff --git a/.gitignore b/.gitignore index b40db10..82abcd3 100644 --- a/.gitignore +++ b/.gitignore @@ -373,4 +373,6 @@ FodyWeavers.xsd *.code-workspace # Strong-Name Key -*.snk \ No newline at end of file +*.snk + +.bot/ \ No newline at end of file diff --git a/.nuget/Codebelt.Extensions.Xunit.App/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.Xunit.App/PackageReleaseNotes.txt index cc50285..3d74020 100644 --- a/.nuget/Codebelt.Extensions.Xunit.App/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.Xunit.App/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version: 11.1.0 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + Version: 11.0.10 Availability: .NET 10 and .NET 9 diff --git a/.nuget/Codebelt.Extensions.Xunit.Hosting.AspNetCore/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.Xunit.Hosting.AspNetCore/PackageReleaseNotes.txt index 6f1f329..172899f 100644 --- a/.nuget/Codebelt.Extensions.Xunit.Hosting.AspNetCore/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.Xunit.Hosting.AspNetCore/PackageReleaseNotes.txt @@ -1,12 +1,22 @@ -Version: 11.0.10 +Version: 11.1.0 Availability: .NET 10 and .NET 9 -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) - -Version: 11.0.9 +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + +# New Features +- ADDED WebApplicationTestFactory class in the Codebelt.Extensions.Xunit.Hosting.AspNetCore namespace for simplified static creation of ASP.NET Core application tests from a TEntryPoint with BlockingManagedWebApplicationFixture{TEntryPoint} as the default fixture +- ADDED WebApplicationTest{TEntryPoint,T}, IWebApplicationFixture{TEntryPoint}, BlockingManagedWebApplicationFixture{TEntryPoint} and WebApplicationFixtureExtensions in the Codebelt.Extensions.Xunit.Hosting.AspNetCore namespace to support Program.cs based ASP.NET Core tests with TestServer + +Version: 11.0.10 Availability: .NET 10 and .NET 9 +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + +Version: 11.0.9 +Availability: .NET 10 and .NET 9 + # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) diff --git a/.nuget/Codebelt.Extensions.Xunit.Hosting/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.Xunit.Hosting/PackageReleaseNotes.txt index 0c1dcc2..2d7d86e 100644 --- a/.nuget/Codebelt.Extensions.Xunit.Hosting/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.Xunit.Hosting/PackageReleaseNotes.txt @@ -1,12 +1,23 @@ -Version: 11.0.10 +Version: 11.1.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0 -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) - -Version: 11.0.9 +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + +# New Features +- ADDED ApplicationHostFactory class in the Codebelt.Extensions.Xunit.Hosting namespace for creating started IHost instances from an application entry point assembly +- ADDED ApplicationTestFactory class in the Codebelt.Extensions.Xunit.Hosting namespace for simplified static creation of application tests from a TEntryPoint with BlockingManagedApplicationFixture{TEntryPoint} as the default fixture +- ADDED ApplicationTest{TEntryPoint,T}, IApplicationFixture{TEntryPoint}, BlockingManagedApplicationFixture{TEntryPoint} and ApplicationFixtureExtensions in the Codebelt.Extensions.Xunit.Hosting namespace to support Program.cs based host tests for console, worker and generic host applications + +Version: 11.0.10 Availability: .NET 10, .NET 9 and .NET Standard 2.0 +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + +Version: 11.0.9 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 + # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) diff --git a/.nuget/Codebelt.Extensions.Xunit/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.Xunit/PackageReleaseNotes.txt index 5a4e24c..26e8f65 100644 --- a/.nuget/Codebelt.Extensions.Xunit/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.Xunit/PackageReleaseNotes.txt @@ -1,17 +1,23 @@ +Version: 11.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + Version: 11.0.10 -Availability: .NET 10 and .NET 9 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) Version: 11.0.9 -Availability: .NET 10 and .NET 9 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) Version: 11.0.8 -Availability: .NET 10 and .NET 9 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) @@ -19,49 +25,49 @@ Availability: .NET 10 and .NET 9 - CHANGED PackageReleaseNotes retrieval in Directory.Build.targets to use simplified System.IO.File.ReadAllText approach Version: 11.0.7 -Availability: .NET 10 and .NET 9 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) Version: 11.0.6 -Availability: .NET 10 and .NET 9 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) Version: 11.0.5 -Availability: .NET 10 and .NET 9 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) Version: 11.0.4 -Availability: .NET 10 and .NET 9 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) Version: 11.0.3 -Availability: .NET 10 and .NET 9 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) Version: 11.0.2 -Availability: .NET 10 and .NET 9 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) Version: 11.0.1 -Availability: .NET 10 and .NET 9 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) Version: 11.0.0 -Availability: .NET 10 and .NET 9 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) @@ -70,49 +76,49 @@ Availability: .NET 10 and .NET 9 - CHANGED Test class in the Codebelt.Extensions.Xunit namespace to use ValueTask for InitializeAsync instead of Task (xUnit v3 consequence change) Version: 10.0.7 -Availability: .NET 9 and .NET 8 +Availability: .NET 9, .NET 8 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) Version: 10.0.6 -Availability: .NET 9 and .NET 8 +Availability: .NET 9, .NET 8 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) Version: 10.0.5 -Availability: .NET 9 and .NET 8 +Availability: .NET 9, .NET 8 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) Version: 10.0.4 -Availability: .NET 9 and .NET 8 +Availability: .NET 9, .NET 8 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) Version: 10.0.3 -Availability: .NET 9 and .NET 8 +Availability: .NET 9, .NET 8 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) Version: 10.0.2 -Availability: .NET 9 and .NET 8 +Availability: .NET 9, .NET 8 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) Version: 10.0.1 -Availability: .NET 9 and .NET 8 +Availability: .NET 9, .NET 8 and .NET Standard 2.0 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) Version: 10.0.0 -Availability: .NET 9 and .NET 8 +Availability: .NET 9, .NET 8 and .NET Standard 2.0 # ALM - CHANGED Dependencies to latest and greatest with respect to TFMs @@ -121,7 +127,7 @@ Availability: .NET 9 and .NET 8 - EXTENDED Test class in the Codebelt.Extensions.Xunit namespace to report unhandled exceptions in the test output using the injected ITestOutputHelper interface Version: 9.1.3 -Availability: .NET 9 and .NET 8 +Availability: .NET 9, .NET 8 and .NET Standard 2.0 # ALM - CHANGED Dependencies to latest and greatest with respect to TFMs diff --git a/AGENTS.md b/AGENTS.md index 0faab13..42d5fbb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -207,6 +207,12 @@ namespace Codebelt.Extensions.Xunit // Same as SUT - Per-package notes in `.nuget//PackageReleaseNotes.txt`. - Keep updated for public API changes. +## Official Documentation + +- Public API conventions belong in `.docfx/api/namespaces/` and should be treated as the official documentation source for library behavior and naming vocabulary. +- When adding or renaming public APIs, update the relevant namespace page in `.docfx/api/namespaces/` if the change introduces or clarifies a convention. +- Keep internal reasoning, exploratory notes, and agent discussion out of DocFX pages; summarize only stable public guidance. + ## Commit Style (Gitmoji) This repo uses **gitmoji** commit messages — do **not** use Conventional Commits (`feat:`, `fix:`, etc.). diff --git a/CHANGELOG.md b/CHANGELOG.md index 494ae73..a35bd85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,33 @@ For more details, please refer to `PackageReleaseNotes.txt` on a per assembly ba > [!NOTE] > Changelog entries prior to version 8.4.0 was migrated from previous versions of Cuemon.Extensions.Xunit, Cuemon.Extensions.Xunit.Hosting, and Cuemon.Extensions.Xunit.Hosting.AspNetCore. +## [11.1.0] - 2026-06-05 + +This is a minor release that brings WebApplicationFactory-like integration testing patterns to the entire .NET application stack — not just ASP.NET Core. ApplicationHostFactory and ApplicationTest abstractions enable Program.cs-based testing for Generic Host scenarios, while WebApplicationTest provides the equivalent TestServer experience for ASP.NET Core. Both patterns support modern minimal hosting and legacy Startup.cs configurations, with comprehensive bootstrapper reference applications and functional test coverage demonstrating real-world testing scenarios. + +### Added + +- ApplicationHostFactory class in the Codebelt.Extensions.Xunit.Hosting namespace that creates started IHost instances from Program.cs entry points, +- ApplicationTest{TEntryPoint,T} base classes in the Codebelt.Extensions.Xunit.Hosting namespace for host integration testing patterns with generic and non-generic variants, +- ApplicationTestFactory class in the Codebelt.Extensions.Xunit.Hosting namespace for static factory methods to create host test instances, +- IApplicationFixture{TEntryPoint} interface in the Codebelt.Extensions.Xunit.Hosting namespace for fixture-based host lifecycle management, +- BlockingManagedApplicationFixture{TEntryPoint} class in the Codebelt.Extensions.Xunit.Hosting namespace providing non-blocking host fixture implementation, +- ApplicationFixtureExtensions class in the Codebelt.Extensions.Xunit.Hosting namespace providing convenient fixture setup methods, +- WebApplicationTest{TEntryPoint,T} base classes in the Codebelt.Extensions.Xunit.Hosting.AspNetCore namespace for ASP.NET Core Program.cs-based TestServer testing, +- WebApplicationTestFactory class in the Codebelt.Extensions.Xunit.Hosting.AspNetCore namespace for static factory methods to create web application test instances, +- IWebApplicationFixture{TEntryPoint} interface in the Codebelt.Extensions.Xunit.Hosting.AspNetCore namespace for web application fixture lifecycle management, +- BlockingManagedWebApplicationFixture{TEntryPoint} class in the Codebelt.Extensions.Xunit.Hosting.AspNetCore namespace providing non-blocking web fixture implementation, +- WebApplicationFixtureExtensions class in the Codebelt.Extensions.Xunit.Hosting.AspNetCore namespace providing convenient web fixture setup methods, +- Eight bootstrapper reference applications demonstrating host patterns: BootstrapperConsole.App (classic Startup pattern), BootstrapperMinimalConsole.App (minimal hosting), BootstrapperWorker.App (BackgroundService with Startup), BootstrapperMinimalWorker.App (minimal worker service), BootstrapperWeb.App (ASP.NET Core with Startup), BootstrapperMinimalWeb.App (ASP.NET Core minimal), BootstrapperClassicProgram.App (top-level statements), and BootstrapperProgram.App (advanced customization), +- Comprehensive functional test coverage for hosting abstractions and integration patterns across Generic Host and ASP.NET Core scenarios, including all bootstrapper configurations. + +### Changed + +- Dependencies upgraded to latest compatible versions: added Codebelt.Bootstrapper.Console (5.1.0), Codebelt.Bootstrapper.Web (5.1.0), and Codebelt.Bootstrapper.Worker (5.1.0) packages; upgraded Codebelt.Extensions.BenchmarkDotNet.Console from 1.2.6 to 1.2.7; upgraded Microsoft.NET.Test.Sdk from 18.5.1 to 18.6.0, +- Solution structure reorganized with new /app/ folder containing eight bootstrapper reference applications, +- Project configuration updated with new Codebelt.Extensions.Xunit.Hosting.FunctionalTests and Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests functional test projects, +- Package release notes for all previous versions updated with consistent availability format information. + ## [11.0.10] - 2026-05-21 This is a patch release focused on codebase modernization, enhanced testing coverage, and developer workflow improvements. @@ -382,7 +409,10 @@ This major release is first and foremost focused on ironing out any wrinkles tha - Added null conditional operator to the ServiceProvider property on the HostFixture class in the Codebelt.Extensions.Xunit.Hosting namespace -[Unreleased]: https://github.com/codebeltnet/xunit/compare/v11.0.10...HEAD + + +[Unreleased]: https://github.com/codebeltnet/xunit/compare/v11.1.0...HEAD +[11.1.0]: https://github.com/codebeltnet/xunit/compare/v11.0.10...v11.1.0 [11.0.10]: https://github.com/codebeltnet/xunit/compare/v11.0.9...v11.0.10 [11.0.9]: https://github.com/codebeltnet/xunit/compare/v11.0.8...v11.0.9 [11.0.8]: https://github.com/codebeltnet/xunit/compare/v11.0.7...v11.0.8 diff --git a/Codebelt.Extensions.Xunit.slnx b/Codebelt.Extensions.Xunit.slnx index 99eeb0b..d6c2e13 100644 --- a/Codebelt.Extensions.Xunit.slnx +++ b/Codebelt.Extensions.Xunit.slnx @@ -1,4 +1,14 @@ + + + + + + + + + + @@ -6,7 +16,9 @@ + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 4494ab0..85efff1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,13 +4,16 @@ true - - - + + + + + + - + @@ -42,4 +45,4 @@ - \ No newline at end of file + diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/BootstrapperConsoleMarker.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/BootstrapperConsoleMarker.cs new file mode 100644 index 0000000..dd6ac36 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/BootstrapperConsoleMarker.cs @@ -0,0 +1,11 @@ +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App; + +public sealed class BootstrapperConsoleMarker +{ + public BootstrapperConsoleMarker(string value) + { + Value = value; + } + + public string Value { get; } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App.csproj b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App.csproj new file mode 100644 index 0000000..84750d0 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App.csproj @@ -0,0 +1,21 @@ + + + + net10.0;net9.0 + Exe + false + false + false + false + true + 0 + none + NU1701,NETSDK1206 + false + + + + + + + diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Program.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Program.cs new file mode 100644 index 0000000..f982687 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Program.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using Codebelt.Bootstrapper.Console; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App; + +public sealed class Program : ConsoleProgram +{ + public static Task Main(string[] args) + { + return CreateHostBuilder(args).Build().RunAsync(); + } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Startup.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Startup.cs new file mode 100644 index 0000000..ff8cb3c --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Startup.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Codebelt.Bootstrapper.Console; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App; + +public sealed class Startup : ConsoleStartup +{ + public Startup(IConfiguration configuration, IHostEnvironment environment) : base(configuration, environment) + { + } + + public override void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(new BootstrapperConsoleMarker("Bootstrapper Console")); + } + + public override void ConfigureConsole(IServiceProvider serviceProvider) + { + } + + public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/BootstrapperMinimalConsoleMarker.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/BootstrapperMinimalConsoleMarker.cs new file mode 100644 index 0000000..82dd16f --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/BootstrapperMinimalConsoleMarker.cs @@ -0,0 +1,11 @@ +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App; + +public sealed class BootstrapperMinimalConsoleMarker +{ + public BootstrapperMinimalConsoleMarker(string value) + { + Value = value; + } + + public string Value { get; } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App.csproj b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App.csproj new file mode 100644 index 0000000..84750d0 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App.csproj @@ -0,0 +1,21 @@ + + + + net10.0;net9.0 + Exe + false + false + false + false + true + 0 + none + NU1701,NETSDK1206 + false + + + + + + + diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/Program.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/Program.cs new file mode 100644 index 0000000..ab69e38 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Codebelt.Bootstrapper.Console; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App; + +public sealed class Program : MinimalConsoleProgram +{ + public static Task Main(string[] args) + { + var builder = CreateHostBuilder(args); + builder.Services.AddSingleton(new BootstrapperMinimalConsoleMarker("Bootstrapper Minimal Console")); + + var host = builder.Build(); + return host.RunAsync(); + } + + public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/BootstrapperMinimalWebMarker.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/BootstrapperMinimalWebMarker.cs new file mode 100644 index 0000000..e376b50 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/BootstrapperMinimalWebMarker.cs @@ -0,0 +1,11 @@ +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App; + +public sealed class BootstrapperMinimalWebMarker +{ + public BootstrapperMinimalWebMarker(string value) + { + Value = value; + } + + public string Value { get; } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App.csproj b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App.csproj new file mode 100644 index 0000000..f3a06e4 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App.csproj @@ -0,0 +1,21 @@ + + + + net10.0;net9.0 + false + Exe + false + false + false + true + 0 + none + NU1701,NETSDK1206 + false + + + + + + + diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/Program.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/Program.cs new file mode 100644 index 0000000..3736b6a --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/Program.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using Codebelt.Bootstrapper.Web; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App; + +public sealed class Program : MinimalWebProgram +{ + public static Task Main(string[] args) + { + var builder = CreateHostBuilder(args); + builder.Services.AddSingleton(new BootstrapperMinimalWebMarker("Bootstrapper Minimal Web")); + + var app = builder.Build(); + + app.MapGet("/", (BootstrapperMinimalWebMarker marker) => marker.Value); + + return app.RunAsync(); + } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/Properties/launchSettings.json b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/Properties/launchSettings.json new file mode 100644 index 0000000..0909904 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Codebelt.Extensions.Xunit.Hosting.AspNetCore.BootstrapperMinimalWebTestSite": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:64266;http://localhost:64268" + } + } +} \ No newline at end of file diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/BootstrapperMinimalWorkerMarker.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/BootstrapperMinimalWorkerMarker.cs new file mode 100644 index 0000000..91950c6 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/BootstrapperMinimalWorkerMarker.cs @@ -0,0 +1,11 @@ +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App; + +public sealed class BootstrapperMinimalWorkerMarker +{ + public BootstrapperMinimalWorkerMarker(string value) + { + Value = value; + } + + public string Value { get; } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/BootstrapperMinimalWorkerService.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/BootstrapperMinimalWorkerService.cs new file mode 100644 index 0000000..b693bf9 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/BootstrapperMinimalWorkerService.cs @@ -0,0 +1,13 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App; + +public sealed class BootstrapperMinimalWorkerService : BackgroundService +{ + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + return Task.CompletedTask; + } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App.csproj b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App.csproj new file mode 100644 index 0000000..2de2a06 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App.csproj @@ -0,0 +1,21 @@ + + + + net10.0;net9.0 + Exe + false + false + false + false + true + 0 + none + NU1701,NETSDK1206 + false + + + + + + + diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/Program.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/Program.cs new file mode 100644 index 0000000..e36ed7e --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/Program.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using Codebelt.Bootstrapper.Worker; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App; + +public sealed class Program : MinimalWorkerProgram +{ + public static Task Main(string[] args) + { + var builder = CreateHostBuilder(args); + builder.Services.AddSingleton(new BootstrapperMinimalWorkerMarker("Bootstrapper Minimal Worker")); + builder.Services.AddHostedService(); + + var host = builder.Build(); + return host.RunAsync(); + } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/BootstrapperWebMarker.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/BootstrapperWebMarker.cs new file mode 100644 index 0000000..79f2e82 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/BootstrapperWebMarker.cs @@ -0,0 +1,11 @@ +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App; + +public sealed class BootstrapperWebMarker +{ + public BootstrapperWebMarker(string value) + { + Value = value; + } + + public string Value { get; } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App.csproj b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App.csproj new file mode 100644 index 0000000..f3a06e4 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App.csproj @@ -0,0 +1,21 @@ + + + + net10.0;net9.0 + false + Exe + false + false + false + true + 0 + none + NU1701,NETSDK1206 + false + + + + + + + diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Program.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Program.cs new file mode 100644 index 0000000..8e6f6ac --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Program.cs @@ -0,0 +1,12 @@ +using Codebelt.Bootstrapper.Web; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App; + +public sealed class Program : WebProgram +{ + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Properties/launchSettings.json b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Properties/launchSettings.json new file mode 100644 index 0000000..76c3923 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Codebelt.Extensions.Xunit.Hosting.AspNetCore.BootstrapperWebTestSite": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:64267;http://localhost:64269" + } + } +} \ No newline at end of file diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Startup.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Startup.cs new file mode 100644 index 0000000..faae51b --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Startup.cs @@ -0,0 +1,33 @@ +using Codebelt.Bootstrapper.Web; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App; + +public sealed class Startup : WebStartup +{ + public Startup(IConfiguration configuration, IHostEnvironment environment) : base(configuration, environment) + { + } + + public override void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(new BootstrapperWebMarker("Bootstrapper Web")); + } + + public override void ConfigurePipeline(IApplicationBuilder app) + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/", async context => + { + var marker = context.RequestServices.GetRequiredService(); + await context.Response.WriteAsync($"{marker.Value}|{Environment.EnvironmentName}").ConfigureAwait(false); + }); + }); + } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/BootstrapperWorkerMarker.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/BootstrapperWorkerMarker.cs new file mode 100644 index 0000000..9475bba --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/BootstrapperWorkerMarker.cs @@ -0,0 +1,11 @@ +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App; + +public sealed class BootstrapperWorkerMarker +{ + public BootstrapperWorkerMarker(string value) + { + Value = value; + } + + public string Value { get; } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/BootstrapperWorkerService.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/BootstrapperWorkerService.cs new file mode 100644 index 0000000..38bb4ab --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/BootstrapperWorkerService.cs @@ -0,0 +1,13 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App; + +public sealed class BootstrapperWorkerService : BackgroundService +{ + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + return Task.CompletedTask; + } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App.csproj b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App.csproj new file mode 100644 index 0000000..2de2a06 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App.csproj @@ -0,0 +1,21 @@ + + + + net10.0;net9.0 + Exe + false + false + false + false + true + 0 + none + NU1701,NETSDK1206 + false + + + + + + + diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/Program.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/Program.cs new file mode 100644 index 0000000..95abd9e --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/Program.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using Codebelt.Bootstrapper.Worker; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App; + +public sealed class Program : WorkerProgram +{ + public static async Task Main(string[] args) + { + await CreateHostBuilder(args).Build().RunAsync().ConfigureAwait(false); + } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/Startup.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/Startup.cs new file mode 100644 index 0000000..11da2b0 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/Startup.cs @@ -0,0 +1,19 @@ +using Codebelt.Bootstrapper.Worker; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App; + +public sealed class Startup : WorkerStartup +{ + public Startup(IConfiguration configuration, IHostEnvironment environment) : base(configuration, environment) + { + } + + public override void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(new BootstrapperWorkerMarker("Bootstrapper Worker")); + services.AddHostedService(); + } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/ClassicProgramMarker.cs b/app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/ClassicProgramMarker.cs new file mode 100644 index 0000000..cc157f7 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/ClassicProgramMarker.cs @@ -0,0 +1,11 @@ +namespace Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App; + +public sealed class ClassicProgramMarker +{ + public ClassicProgramMarker(string value) + { + Value = value; + } + + public string Value { get; } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App.csproj b/app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App.csproj new file mode 100644 index 0000000..61b3e41 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App.csproj @@ -0,0 +1,17 @@ + + + + net10.0;net9.0 + false + Exe + false + false + false + true + 0 + none + NU1701,NETSDK1206 + false + + + diff --git a/app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/Program.cs b/app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/Program.cs new file mode 100644 index 0000000..619f8b2 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/Program.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App; + +public sealed class Program +{ + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(builder => + { + builder.ConfigureServices(services => services.AddSingleton(new ClassicProgramMarker("Classic Program"))); + builder.Configure(app => app.Run(async context => + { + var marker = context.RequestServices.GetRequiredService(); + await context.Response.WriteAsync(marker.Value).ConfigureAwait(false); + })); + }); + } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/Properties/launchSettings.json b/app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/Properties/launchSettings.json new file mode 100644 index 0000000..45a391b --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Codebelt.Extensions.Xunit.Hosting.AspNetCore.ClassicProgramTestSite": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:64340;http://localhost:64341" + } + } +} \ No newline at end of file diff --git a/app/Codebelt.Extensions.Xunit.Hosting.Program.App/Codebelt.Extensions.Xunit.Hosting.Program.App.csproj b/app/Codebelt.Extensions.Xunit.Hosting.Program.App/Codebelt.Extensions.Xunit.Hosting.Program.App.csproj new file mode 100644 index 0000000..61b3e41 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.Program.App/Codebelt.Extensions.Xunit.Hosting.Program.App.csproj @@ -0,0 +1,17 @@ + + + + net10.0;net9.0 + false + Exe + false + false + false + true + 0 + none + NU1701,NETSDK1206 + false + + + diff --git a/app/Codebelt.Extensions.Xunit.Hosting.Program.App/Program.cs b/app/Codebelt.Extensions.Xunit.Hosting.Program.App/Program.cs new file mode 100644 index 0000000..feda131 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.Program.App/Program.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.Program.App; + +public sealed class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddSingleton(new ProgramMarker("Modern Program")); + + var app = builder.Build(); + + app.MapGet("/", (ProgramMarker marker, IHostEnvironment environment) => $"{marker.Value}|{environment.EnvironmentName}"); + app.MapGet("/configuration", (IConfiguration configuration) => configuration["ProgramLane:Message"] ?? "Missing"); + app.MapGet("/custom-service", (IEnumerable customizations) => + { + foreach (var customization in customizations) + { + return customization.Value; + } + + return "Missing"; + }); + + app.Run(); + } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.Program.App/ProgramCustomization.cs b/app/Codebelt.Extensions.Xunit.Hosting.Program.App/ProgramCustomization.cs new file mode 100644 index 0000000..f9a3b9e --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.Program.App/ProgramCustomization.cs @@ -0,0 +1,11 @@ +namespace Codebelt.Extensions.Xunit.Hosting.Program.App; + +public sealed class ProgramCustomization +{ + public ProgramCustomization(string value) + { + Value = value; + } + + public string Value { get; } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.Program.App/ProgramMarker.cs b/app/Codebelt.Extensions.Xunit.Hosting.Program.App/ProgramMarker.cs new file mode 100644 index 0000000..58eec85 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.Program.App/ProgramMarker.cs @@ -0,0 +1,11 @@ +namespace Codebelt.Extensions.Xunit.Hosting.Program.App; + +public sealed class ProgramMarker +{ + public ProgramMarker(string value) + { + Value = value; + } + + public string Value { get; } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.Program.App/Properties/launchSettings.json b/app/Codebelt.Extensions.Xunit.Hosting.Program.App/Properties/launchSettings.json new file mode 100644 index 0000000..3a54a51 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.Program.App/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Codebelt.Extensions.Xunit.Hosting.AspNetCore.ProgramTestSite": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:64346;http://localhost:64347" + } + } +} \ No newline at end of file diff --git a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/BlockingManagedWebApplicationFixture.cs b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/BlockingManagedWebApplicationFixture.cs new file mode 100644 index 0000000..64f2886 --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/BlockingManagedWebApplicationFixture.cs @@ -0,0 +1,76 @@ +using Codebelt.Extensions.Xunit.Hosting.AspNetCore.Internal; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; + +/// +/// Provides a blocking managed implementation of the interface. +/// +/// A type in the entry point assembly of the application. +/// +/// +/// +/// Unlike the base managed web host fixtures, this fixture starts the resolved application host synchronously. +/// ASP.NET Core application entry point testing must expose a started after fixture initialization. +/// There is no separate non-blocking managed web application fixture because this application-entry-point API is new and unreleased. +/// +public class BlockingManagedWebApplicationFixture : HostFixture, IWebApplicationFixture where TEntryPoint : class +{ + /// + /// Initializes a new instance of the class. + /// + public BlockingManagedWebApplicationFixture() + { + AsyncHostRunnerCallback = (host, _) => + { + host.Start(); + return Task.CompletedTask; + }; + } + + /// + /// Creates and configures the of this instance. + /// + /// The object that inherits from . + /// was added to support those cases where the caller is required in the host configuration. + /// + /// is null. + /// + /// + /// is not assignable from . + /// + public virtual void ConfigureHost(Test hostTest) + { + ArgumentNullException.ThrowIfNull(hostTest); + if (!HasTypes(hostTest.GetType(), typeof(WebApplicationTest<,>))) { throw new ArgumentOutOfRangeException(nameof(hostTest), typeof(WebApplicationTest<,>), $"{nameof(hostTest)} is not assignable from WebApplicationTest."); } + if (this.HasValidState()) { return; } + + Host = WebApplicationHostFactory.Create(ConfigureWebHostCallback); + Server = Host.GetTestServer(); + Configuration = Host.Services.GetRequiredService(); + Environment = Host.Services.GetRequiredService(); + + ConfigureCallback(Configuration, Environment); + + AsyncHostRunnerCallback(Host, CancellationToken.None); + } + + /// + /// Gets or sets the delegate that provides a way to override the before the application is built. + /// + /// The delegate that provides a way to override the . + public Action ConfigureWebHostCallback { get; set; } + + /// + /// Gets the initialized by this instance. + /// + /// The initialized by this instance. + public TestServer Server { get; protected set; } +} diff --git a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/IWebApplicationFixture.cs b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/IWebApplicationFixture.cs new file mode 100644 index 0000000..2cfc9da --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/IWebApplicationFixture.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; + +/// +/// Provides a way to use Microsoft Dependency Injection in tests that bootstrap an existing ASP.NET Core application entry point. +/// +/// A type in the entry point assembly of the application. +/// +public interface IWebApplicationFixture : IHostFixture where TEntryPoint : class +{ + /// + /// Gets or sets the delegate that provides a way to override the before the application is built. + /// + /// The delegate that provides a way to override the . + Action ConfigureWebHostCallback { get; set; } + + /// + /// Gets the initialized by this instance. + /// + /// The initialized by this instance. + TestServer Server { get; } + + /// + /// Creates and configures the of this . + /// + /// The object that inherits from . + /// was added to support those cases where the caller is required in the host configuration. + void ConfigureHost(Test hostTest); +} diff --git a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Internal/WebApplicationHostFactory.cs b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Internal/WebApplicationHostFactory.cs new file mode 100644 index 0000000..cd0528b --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Internal/WebApplicationHostFactory.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore.Internal; + +internal static class WebApplicationHostFactory +{ + public static IHost Create(Action configureWebHost) where TEntryPoint : class + { + return ApplicationHostFactory.Create(hostBuilder => hostBuilder.ConfigureWebHost(webHostBuilder => + { + webHostBuilder.UseTestServer(o => o.PreserveExecutionContext = true); + configureWebHost?.Invoke(webHostBuilder); + }), false); + } +} diff --git a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Internal/WebApplicationTest.cs b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Internal/WebApplicationTest.cs new file mode 100644 index 0000000..1c80630 --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Internal/WebApplicationTest.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore.Internal; + +internal sealed class WebApplicationTest : WebApplicationTest> where TEntryPoint : class +{ + private readonly Action _webHostConfigurator; + private readonly IWebApplicationFixture _hostFixture; + + internal WebApplicationTest(Action webHostConfigurator, IWebApplicationFixture hostFixture) : base(true, hostFixture, callerType: webHostConfigurator?.Target?.GetType()) + { + _webHostConfigurator = webHostConfigurator; + _hostFixture = hostFixture; + InitializeHostFixture(hostFixture); + } + + private void InitializeHostFixture(IWebApplicationFixture hostFixture) + { + if (!hostFixture.HasValidState()) + { + hostFixture.ConfigureCallback = Configure; + hostFixture.ConfigureWebHostCallback = ConfigureWebHost; + hostFixture.ConfigureHost(this); + } + Host = hostFixture.Host; + Server = hostFixture.Server; + Configure(hostFixture.Configuration, hostFixture.Environment); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + _webHostConfigurator?.Invoke(builder); + } + + protected override void OnDisposeManagedResources() + { + _hostFixture.Dispose(); + base.OnDisposeManagedResources(); + } + + protected override async ValueTask OnDisposeManagedResourcesAsync() + { + await _hostFixture.DisposeAsync().ConfigureAwait(false); + await base.OnDisposeManagedResourcesAsync().ConfigureAwait(false); + } +} diff --git a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/WebApplicationFixtureExtensions.cs b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/WebApplicationFixtureExtensions.cs new file mode 100644 index 0000000..7d34819 --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/WebApplicationFixtureExtensions.cs @@ -0,0 +1,24 @@ +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; + +/// +/// Extension methods for the interface. +/// +public static class WebApplicationFixtureExtensions +{ + /// + /// Determines whether the specified has a valid state. + /// + /// A type in the entry point assembly of the application. + /// The to check. + /// true if the specified has a valid state; otherwise, false. + /// + /// A valid state is defined as having non-null values for the following properties: + /// , and . + /// + public static bool HasValidState(this IWebApplicationFixture hostFixture) where TEntryPoint : class + { + return hostFixture.Host != null && + hostFixture.ConfigureCallback != null && + hostFixture.ConfigureWebHostCallback != null; + } +} diff --git a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/WebApplicationTest.cs b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/WebApplicationTest.cs new file mode 100644 index 0000000..623cbce --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/WebApplicationTest.cs @@ -0,0 +1,65 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; + +/// +/// Represents a base class from which all implementations of unit testing, that uses Microsoft Dependency Injection and depends on an existing ASP.NET Core application entry point, should derive. +/// +/// A type in the entry point assembly of the application. +/// The type of the object that implements the interface. +/// +/// +public abstract class WebApplicationTest : HostTest, IClassFixture where TEntryPoint : class where T : class, IWebApplicationFixture +{ + /// + /// Initializes a new instance of the class. + /// + /// An implementation of the interface. + /// An implementation of the interface. + /// The of caller that ends up invoking this instance. + protected WebApplicationTest(T hostFixture, ITestOutputHelper output = null, Type callerType = null) : this(false, hostFixture, output, callerType) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// A value indicating whether to skip the host fixture initialization. + /// An implementation of the interface. + /// An implementation of the interface. + /// The of caller that ends up invoking this instance. + /// + /// is null. + /// + protected WebApplicationTest(bool skipHostFixtureInitialization, T hostFixture, ITestOutputHelper output = null, Type callerType = null) : base(output, callerType) + { + ArgumentNullException.ThrowIfNull(hostFixture); + if (skipHostFixtureInitialization) { return; } + if (!hostFixture.HasValidState()) + { + hostFixture.ConfigureCallback = Configure; + hostFixture.ConfigureWebHostCallback = ConfigureWebHost; + hostFixture.ConfigureHost(this); + } + Host = hostFixture.Host; + Server = hostFixture.Server; + Configure(hostFixture.Configuration, hostFixture.Environment); + } + + /// + /// Gets the initialized by the . + /// + /// The initialized by the . + public TestServer Server { get; protected set; } + + /// + /// Provides a way to override the defaults before the application is built. + /// + /// The used to configure the application. + protected virtual void ConfigureWebHost(IWebHostBuilder builder) + { + } +} diff --git a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/WebApplicationTestFactory.cs b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/WebApplicationTestFactory.cs new file mode 100644 index 0000000..84c19fe --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/WebApplicationTestFactory.cs @@ -0,0 +1,40 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; + +/// +/// Provides a set of static methods for ASP.NET Core testing that bootstraps an existing application entry point. +/// +/// . +public static class WebApplicationTestFactory +{ + /// + /// Creates and returns an implementation. + /// + /// A type in the entry point assembly of the application. + /// The which may be configured. + /// An optional implementation to use instead of the default instance. + /// An instance of an implementation. + public static IHostTest Create(Action webHostSetup = null, IWebApplicationFixture hostFixture = null) where TEntryPoint : class + { + return new Internal.WebApplicationTest(webHostSetup, hostFixture ?? new BlockingManagedWebApplicationFixture()); + } + + /// + /// Runs an ASP.NET Core application and returns an for the test server. + /// + /// A type in the entry point assembly of the application. + /// The which may be configured. + /// The function delegate that creates a from the . Default is a GET request to the root URL ("/"). + /// An optional implementation to use instead of the default instance. + /// A that represents the asynchronous operation. The task result contains the for the test server. + public static async Task RunAsync(Action webHostSetup = null, Func> responseFactory = null, IWebApplicationFixture hostFixture = null) where TEntryPoint : class + { + using var client = Create(webHostSetup, hostFixture).Host.GetTestClient(); + return await client.ToHttpResponseMessageAsync(responseFactory).ConfigureAwait(false); + } +} diff --git a/src/Codebelt.Extensions.Xunit.Hosting/ApplicationFixtureExtensions.cs b/src/Codebelt.Extensions.Xunit.Hosting/ApplicationFixtureExtensions.cs new file mode 100644 index 0000000..064e6ce --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting/ApplicationFixtureExtensions.cs @@ -0,0 +1,24 @@ +namespace Codebelt.Extensions.Xunit.Hosting; + +/// +/// Extension methods for the interface. +/// +public static class ApplicationFixtureExtensions +{ + /// + /// Determines whether the specified has a valid state. + /// + /// A type in the entry point assembly of the application. + /// The to check. + /// true if the specified has a valid state; otherwise, false. + /// + /// A valid state is defined as having non-null values for the following properties: + /// , and . + /// + public static bool HasValidState(this IApplicationFixture hostFixture) where TEntryPoint : class + { + return hostFixture.Host != null && + hostFixture.ConfigureCallback != null && + hostFixture.ConfigureHostCallback != null; + } +} diff --git a/src/Codebelt.Extensions.Xunit.Hosting/ApplicationHostFactory.cs b/src/Codebelt.Extensions.Xunit.Hosting/ApplicationHostFactory.cs new file mode 100644 index 0000000..022048d --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting/ApplicationHostFactory.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using Codebelt.Extensions.Xunit.Hosting.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting; + +/// +/// Provides factory methods for creating application hosts from an entry point assembly. +/// +public static class ApplicationHostFactory +{ + /// + /// Creates, configures, builds and starts an from the assembly containing . + /// + /// A type in the entry point assembly of the application. + /// The delegate that provides a way to override the before the application is built. + /// A started instance. + /// + /// The entry point assembly does not expose a supported application host. + /// + public static IHost Create(Action configureHost) where TEntryPoint : class + { + return Create(configureHost, true); + } + + /// + /// Creates, configures, builds and starts an from the assembly containing . + /// + /// A type in the entry point assembly of the application. + /// The delegate that provides a way to override the before the application is built. + /// A value indicating whether the entry point should be stopped after the host is built. + /// A started instance. + /// + /// The entry point assembly does not expose a supported application host. + /// + public static IHost Create(Action configureHost, bool stopApplication) where TEntryPoint : class + { + var assembly = typeof(TEntryPoint).Assembly; + var hostBuilder = ProgramHostFactoryResolver.ResolveHostBuilderFactory(assembly)?.Invoke(Array.Empty()); + + if (hostBuilder != null) + { + hostBuilder.UseEnvironment(Environments.Development); + return BuildHost(hostBuilder, configureHost); + } + + var deferredHostBuilder = new DeferredHostBuilder(); + + deferredHostBuilder.UseEnvironment(Environments.Development); + deferredHostBuilder.ConfigureHostConfiguration(config => + { + config.AddInMemoryCollection(new Dictionary + { + [HostDefaults.ApplicationKey] = assembly.GetName().Name + }); + }); + + var hostFactory = ProgramHostFactoryResolver.ResolveHostFactory(assembly, stopApplication, deferredHostBuilder.ConfigureHostBuilder, deferredHostBuilder.EntryPointCompleted); + if (hostFactory == null) + { + throw new InvalidOperationException($"The entry point assembly '{assembly.GetName().Name}' does not expose a supported application host."); + } + + deferredHostBuilder.SetHostFactory(hostFactory); + return BuildHost(deferredHostBuilder, configureHost); + } + + private static IHost BuildHost(IHostBuilder hostBuilder, Action configureHost) + { + configureHost?.Invoke(hostBuilder); + +#if NET9_0_OR_GREATER + hostBuilder.UseDefaultServiceProvider(o => + { + o.ValidateOnBuild = true; + o.ValidateScopes = true; + }); +#endif + + var host = hostBuilder.Build(); + if (hostBuilder is IDisposable disposable) + { + disposable.Dispose(); + } + + return host; + } +} diff --git a/src/Codebelt.Extensions.Xunit.Hosting/ApplicationTest.cs b/src/Codebelt.Extensions.Xunit.Hosting/ApplicationTest.cs new file mode 100644 index 0000000..1e8b61f --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting/ApplicationTest.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Codebelt.Extensions.Xunit.Hosting; + +/// +/// Represents a base class from which all implementations of unit testing, that uses Microsoft Dependency Injection and depends on an existing .NET application entry point, should derive. +/// +/// A type in the entry point assembly of the application. +/// The type of the object that implements the interface. +/// +/// +public abstract class ApplicationTest : HostTest, IClassFixture where TEntryPoint : class where T : class, IApplicationFixture +{ + /// + /// Initializes a new instance of the class. + /// + /// An implementation of the interface. + /// An implementation of the interface. + /// The of caller that ends up invoking this instance. + protected ApplicationTest(T hostFixture, ITestOutputHelper output = null, Type callerType = null) : this(false, hostFixture, output, callerType) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// A value indicating whether to skip the host fixture initialization. + /// An implementation of the interface. + /// An implementation of the interface. + /// The of caller that ends up invoking this instance. + /// + /// is null. + /// + protected ApplicationTest(bool skipHostFixtureInitialization, T hostFixture, ITestOutputHelper output = null, Type callerType = null) : base(output, callerType) + { +#if NETSTANDARD2_0 + if (hostFixture == null) { throw new ArgumentNullException(nameof(hostFixture)); } +#else + ArgumentNullException.ThrowIfNull(hostFixture); +#endif + if (skipHostFixtureInitialization) { return; } + if (!hostFixture.HasValidState()) + { + hostFixture.ConfigureCallback = Configure; + hostFixture.ConfigureHostCallback = ConfigureHost; + hostFixture.ConfigureHost(this); + } + Host = hostFixture.Host; + Configure(hostFixture.Configuration, hostFixture.Environment); + } + + /// + /// Provides a way to override the defaults before the application is built. + /// + /// The used to configure the application. + protected virtual void ConfigureHost(IHostBuilder builder) + { + } +} diff --git a/src/Codebelt.Extensions.Xunit.Hosting/ApplicationTestFactory.cs b/src/Codebelt.Extensions.Xunit.Hosting/ApplicationTestFactory.cs new file mode 100644 index 0000000..d2cef04 --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting/ApplicationTestFactory.cs @@ -0,0 +1,22 @@ +using System; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting; + +/// +/// Provides a set of static methods for testing that bootstraps an existing .NET application entry point. +/// +public static class ApplicationTestFactory +{ + /// + /// Creates and returns an implementation. + /// + /// A type in the entry point assembly of the application. + /// The which may be configured. + /// An optional implementation to use instead of the default instance. + /// An instance of an implementation. + public static IHostTest Create(Action hostSetup = null, IApplicationFixture hostFixture = null) where TEntryPoint : class + { + return new Internal.ApplicationTest(hostSetup, hostFixture ?? new BlockingManagedApplicationFixture()); + } +} diff --git a/src/Codebelt.Extensions.Xunit.Hosting/BlockingManagedApplicationFixture.cs b/src/Codebelt.Extensions.Xunit.Hosting/BlockingManagedApplicationFixture.cs new file mode 100644 index 0000000..40fd04d --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting/BlockingManagedApplicationFixture.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Codebelt.Extensions.Xunit.Hosting; + +/// +/// Provides a blocking managed implementation of the interface. +/// +/// A type in the entry point assembly of the application. +/// +/// +/// +/// Unlike the base managed host fixtures, this fixture starts the resolved application host synchronously. +/// Application entry point testing must expose a fully started host after fixture initialization. +/// +public class BlockingManagedApplicationFixture : HostFixture, IApplicationFixture where TEntryPoint : class +{ + /// + /// Initializes a new instance of the class. + /// + public BlockingManagedApplicationFixture() + { + AsyncHostRunnerCallback = (host, _) => + { + host.Start(); + return Task.CompletedTask; + }; + } + + /// + /// Creates and configures the of this instance. + /// + /// The object that inherits from . + /// was added to support those cases where the caller is required in the host configuration. + /// + /// is null. + /// + /// + /// is not assignable from . + /// + public virtual void ConfigureHost(Test hostTest) + { +#if NETSTANDARD2_0 + if (hostTest == null) { throw new ArgumentNullException(nameof(hostTest)); } +#else + ArgumentNullException.ThrowIfNull(hostTest); +#endif + if (!HasTypes(hostTest.GetType(), typeof(ApplicationTest<,>))) { throw new ArgumentOutOfRangeException(nameof(hostTest), typeof(ApplicationTest<,>), $"{nameof(hostTest)} is not assignable from ApplicationTest."); } + if (this.HasValidState()) { return; } + + Host = ApplicationHostFactory.Create(ConfigureHostCallback); + Configuration = Host.Services.GetRequiredService(); + Environment = Host.Services.GetRequiredService(); + + ConfigureCallback(Configuration, Environment); + + AsyncHostRunnerCallback(Host, CancellationToken.None); + } + + /// + /// Gets or sets the delegate that provides a way to override the before the application is built. + /// + /// The delegate that provides a way to override the . + public Action ConfigureHostCallback { get; set; } +} diff --git a/src/Codebelt.Extensions.Xunit.Hosting/IApplicationFixture.cs b/src/Codebelt.Extensions.Xunit.Hosting/IApplicationFixture.cs new file mode 100644 index 0000000..0ecb815 --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting/IApplicationFixture.cs @@ -0,0 +1,25 @@ +using System; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting; + +/// +/// Provides a way to use Microsoft Dependency Injection in tests that bootstrap an existing .NET application entry point. +/// +/// A type in the entry point assembly of the application. +/// +public interface IApplicationFixture : IHostFixture where TEntryPoint : class +{ + /// + /// Gets or sets the delegate that provides a way to override the before the application is built. + /// + /// The delegate that provides a way to override the . + Action ConfigureHostCallback { get; set; } + + /// + /// Creates and configures the of this . + /// + /// The object that inherits from . + /// was added to support those cases where the caller is required in the host configuration. + void ConfigureHost(Test hostTest); +} diff --git a/src/Codebelt.Extensions.Xunit.Hosting/Internal/ApplicationTest.cs b/src/Codebelt.Extensions.Xunit.Hosting/Internal/ApplicationTest.cs new file mode 100644 index 0000000..50a64e3 --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting/Internal/ApplicationTest.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.Internal; + +internal sealed class ApplicationTest : ApplicationTest> where TEntryPoint : class +{ + private readonly Action _hostConfigurator; + private readonly IApplicationFixture _hostFixture; + + internal ApplicationTest(Action hostConfigurator, IApplicationFixture hostFixture) : base(true, hostFixture, callerType: hostConfigurator?.Target?.GetType()) + { + _hostConfigurator = hostConfigurator; + _hostFixture = hostFixture; + InitializeHostFixture(hostFixture); + } + + private void InitializeHostFixture(IApplicationFixture hostFixture) + { + if (!hostFixture.HasValidState()) + { + hostFixture.ConfigureCallback = Configure; + hostFixture.ConfigureHostCallback = ConfigureHost; + hostFixture.ConfigureHost(this); + } + Host = hostFixture.Host; + Configure(hostFixture.Configuration, hostFixture.Environment); + } + + protected override void ConfigureHost(IHostBuilder builder) + { + _hostConfigurator?.Invoke(builder); + } + + protected override void OnDisposeManagedResources() + { + _hostFixture.Dispose(); + base.OnDisposeManagedResources(); + } + + protected override async ValueTask OnDisposeManagedResourcesAsync() + { + await _hostFixture.DisposeAsync().ConfigureAwait(false); + await base.OnDisposeManagedResourcesAsync().ConfigureAwait(false); + } +} diff --git a/src/Codebelt.Extensions.Xunit.Hosting/Internal/DeferredHostBuilder.cs b/src/Codebelt.Extensions.Xunit.Hosting/Internal/DeferredHostBuilder.cs new file mode 100644 index 0000000..eccb6c1 --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting/Internal/DeferredHostBuilder.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.Internal; + +// Adapted from the ASP.NET Core testing infrastructure. +// Licensed to the .NET Foundation under one or more agreements under the MIT license. +internal sealed class DeferredHostBuilder : IHostBuilder, IDisposable +{ + private readonly ConfigurationManager _hostConfiguration = new(); + private readonly TaskCompletionSource _hostStarted = new(TaskCreationOptions.RunContinuationsAsynchronously); + private Action _configure; + private Func _hostFactory; + + public DeferredHostBuilder() + { + _configure = builder => + { + foreach (var pair in Properties) + { + builder.Properties[pair.Key] = pair.Value; + } + }; + } + + public IDictionary Properties { get; } = new Dictionary(); + + public IHost Build() + { + var args = new List(); + + foreach (var pair in _hostConfiguration.AsEnumerable()) + { + args.Add($"--{pair.Key}={pair.Value}"); + } + + var host = (IHost)_hostFactory(args.ToArray()); + return new DeferredHost(host, _hostStarted); + } + + public IHostBuilder ConfigureAppConfiguration(Action configureDelegate) + { + _configure += builder => builder.ConfigureAppConfiguration(configureDelegate); + return this; + } + + public IHostBuilder ConfigureContainer(Action configureDelegate) + { + _configure += builder => builder.ConfigureContainer(configureDelegate); + return this; + } + + public IHostBuilder ConfigureHostConfiguration(Action configureDelegate) + { + configureDelegate(_hostConfiguration); + return this; + } + + public IHostBuilder ConfigureServices(Action configureDelegate) + { + _configure += builder => builder.ConfigureServices(configureDelegate); + return this; + } + + public IHostBuilder UseServiceProviderFactory(IServiceProviderFactory factory) where TContainerBuilder : notnull + { + _configure += builder => builder.UseServiceProviderFactory(factory); + return this; + } + + public IHostBuilder UseServiceProviderFactory(Func> factory) where TContainerBuilder : notnull + { + _configure += builder => builder.UseServiceProviderFactory(factory); + return this; + } + + public void ConfigureHostBuilder(object hostBuilder) + { + _configure((IHostBuilder)hostBuilder); + } + + public void EntryPointCompleted(Exception exception) + { + if (exception == null) + { + _hostStarted.TrySetResult(null); + return; + } + + _hostStarted.TrySetException(exception); + } + + public void SetHostFactory(Func hostFactory) + { + _hostFactory = hostFactory; + } + + public void Dispose() + { + _hostConfiguration.Dispose(); + } + + private sealed class DeferredHost : IHost, IAsyncDisposable + { + private readonly IHost _host; + private readonly TaskCompletionSource _hostStarted; + + public DeferredHost(IHost host, TaskCompletionSource hostStarted) + { + _host = host; + _hostStarted = hostStarted; + } + + public IServiceProvider Services => _host.Services; + + public void Dispose() + { + _host.Dispose(); + } + + public async ValueTask DisposeAsync() + { + if (_host is IAsyncDisposable disposable) + { + await disposable.DisposeAsync().ConfigureAwait(false); + return; + } + + Dispose(); + } + + public async Task StartAsync(CancellationToken cancellationToken = default) + { + using var registration = cancellationToken.Register(() => _hostStarted.TrySetCanceled()); + using var startedRegistration = _host.Services.GetRequiredService().ApplicationStarted.Register(() => _hostStarted.TrySetResult(null)); + + await _hostStarted.Task.ConfigureAwait(false); + } + + public Task StopAsync(CancellationToken cancellationToken = default) + { + return _host.StopAsync(cancellationToken); + } + } +} diff --git a/src/Codebelt.Extensions.Xunit.Hosting/Internal/ProgramHostFactoryResolver.cs b/src/Codebelt.Extensions.Xunit.Hosting/Internal/ProgramHostFactoryResolver.cs new file mode 100644 index 0000000..8d47fb7 --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting/Internal/ProgramHostFactoryResolver.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.Internal; + +// Adapted from Microsoft.Extensions.Hosting.HostFactoryResolver. +// Licensed to the .NET Foundation under one or more agreements under the MIT license. +internal static class ProgramHostFactoryResolver +{ + private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; + private static readonly TimeSpan DefaultWaitTimeout = Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromMinutes(5); + + public static Func ResolveHostBuilderFactory(Assembly assembly) + { + return ResolveFactory(assembly, "CreateHostBuilder"); + } + + public static Func ResolveHostFactory(Assembly assembly, bool stopApplication, Action configureHostBuilder, Action entryPointCompleted) + { + if (assembly.EntryPoint == null) { return null; } + + try + { + var hostingAssembly = Assembly.Load("Microsoft.Extensions.Hosting"); + if (hostingAssembly.GetName().Version is Version version && version.Major < 6) { return null; } + } + catch + { + return null; + } + + return args => new HostingListener(args, assembly.EntryPoint, DefaultWaitTimeout, stopApplication, configureHostBuilder, entryPointCompleted).CreateHost(); + } + + private static Func ResolveFactory(Assembly assembly, string name) + { + var programType = assembly.EntryPoint?.DeclaringType; + if (programType == null) { return null; } + + var factory = programType.GetMethod(name, DeclaredOnlyLookup); + if (!IsFactory(factory)) { return null; } + + return args => (T)factory.Invoke(null, new object[] { args }); + } + + private static bool IsFactory(MethodInfo factory) + { + return factory != null && + typeof(T).IsAssignableFrom(factory.ReturnType) && + factory.GetParameters().Length == 1 && + typeof(string[]).Equals(factory.GetParameters()[0].ParameterType); + } + + private sealed class HostingListener : IObserver, IObserver> + { + private static readonly AsyncLocal CurrentListener = new(); + + private readonly string[] _args; + private readonly Action _configure; + private readonly Action _entryPointCompleted; + private readonly MethodInfo _entryPoint; + private readonly TaskCompletionSource _host = new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly bool _stopApplication; + private readonly TimeSpan _waitTimeout; + private IDisposable _disposable; + + public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action configure, Action entryPointCompleted) + { + _args = args; + _entryPoint = entryPoint; + _waitTimeout = waitTimeout; + _stopApplication = stopApplication; + _configure = configure; + _entryPointCompleted = entryPointCompleted; + } + + public object CreateHost() + { + using var subscription = DiagnosticListener.AllListeners.Subscribe(this); + var thread = new Thread(InvokeEntryPoint) + { + IsBackground = true + }; + + thread.Start(); + + if (!_host.Task.Wait(_waitTimeout)) + { + throw new InvalidOperationException($"Timed out waiting for the entry point to build the IHost after {DefaultWaitTimeout}."); + } + + return _host.Task.GetAwaiter().GetResult(); + } + + public void OnCompleted() + { + _disposable?.Dispose(); + } + + public void OnError(Exception error) + { + } + + public void OnNext(DiagnosticListener value) + { + if (CurrentListener.Value != this) { return; } + if (value.Name == "Microsoft.Extensions.Hosting") + { + _disposable = value.Subscribe(this); + } + } + + public void OnNext(KeyValuePair value) + { + if (CurrentListener.Value != this) { return; } + + if (value.Key == "HostBuilding") + { + _configure?.Invoke(value.Value); + } + + if (value.Key == "HostBuilt") + { + _host.TrySetResult(value.Value); + if (_stopApplication) + { + throw new HostAbortedException(); + } + } + } + + private void InvokeEntryPoint() + { + Exception exception = null; + try + { + CurrentListener.Value = this; + var parameters = _entryPoint.GetParameters(); + var result = parameters.Length == 0 + ? _entryPoint.Invoke(null, Array.Empty()) + : _entryPoint.Invoke(null, new object[] { _args }); + + if (result is Task task) + { + task.GetAwaiter().GetResult(); + } + + _host.TrySetException(new InvalidOperationException("The entry point exited without ever building an IHost.")); + } + catch (TargetInvocationException ex) when (ex.InnerException?.GetType().Name == nameof(HostAbortedException)) + { + } + catch (HostAbortedException) + { + } + catch (TargetInvocationException ex) + { + exception = ex.InnerException ?? ex; + _host.TrySetException(exception); + } + catch (Exception ex) + { + exception = ex; + _host.TrySetException(ex); + } + finally + { + CurrentListener.Value = null; + _entryPointCompleted?.Invoke(exception); + } + } + + private sealed class HostAbortedException : Exception + { + } + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BlockingManagedWebApplicationFixtureTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BlockingManagedWebApplicationFixtureTest.cs new file mode 100644 index 0000000..fd65d89 --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BlockingManagedWebApplicationFixtureTest.cs @@ -0,0 +1,42 @@ +using System; +using Xunit; +using ModernProgram = Codebelt.Extensions.Xunit.Hosting.Program.App.Program; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; + +public class BlockingManagedWebApplicationFixtureTest : Test +{ + public BlockingManagedWebApplicationFixtureTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void ConfigureHost_ShouldThrowArgumentOutOfRangeException_WhenHostTestIsNotWebApplicationTest() + { + var fixture = new BlockingManagedWebApplicationFixture(); + + var ex = Assert.Throws(() => fixture.ConfigureHost(this)); + + Assert.Equal("hostTest", ex.ParamName); + } + + [Fact] + public void ConfigureHost_ShouldThrowInvalidOperationException_WhenEntryPointAssemblyHasNoHost() + { + var fixture = new BlockingManagedWebApplicationFixture(); + var test = new DeferredInvalidWebApplicationTest(fixture); + + fixture.ConfigureCallback = test.Configure; + fixture.ConfigureWebHostCallback = test.Configure; + + Assert.Throws(() => fixture.ConfigureHost(test)); + } + + [Fact] + public void HasValidState_ShouldReturnFalse_WhenFixtureIsUninitialized() + { + var fixture = new BlockingManagedWebApplicationFixture(); + + Assert.False(fixture.HasValidState()); + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperMinimalWebApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperMinimalWebApplicationTestTest.cs new file mode 100644 index 0000000..c561553 --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperMinimalWebApplicationTestTest.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using BootstrapperMinimalWebMarker = Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App.BootstrapperMinimalWebMarker; +using BootstrapperMinimalWebProgram = Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App.Program; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; + +public class BootstrapperMinimalWebApplicationTestTest : WebApplicationTest> +{ + public BootstrapperMinimalWebApplicationTestTest(BlockingManagedWebApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + { + } + + [Fact] + public async Task ShouldBootstrapMinimalWebProgram() + { + using var client = Server.CreateClient(); + + var response = await client.GetAsync("/").ConfigureAwait(false); + var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("Bootstrapper Minimal Web", body); + Assert.Equal("Bootstrapper Minimal Web", Host.Services.GetRequiredService().Value); + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperWebApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperWebApplicationTestTest.cs new file mode 100644 index 0000000..7dd7bc7 --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperWebApplicationTestTest.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using BootstrapperWebMarker = Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App.BootstrapperWebMarker; +using BootstrapperWebProgram = Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App.Program; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; + +public class BootstrapperWebApplicationTestTest : WebApplicationTest> +{ + public BootstrapperWebApplicationTestTest(BlockingManagedWebApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + { + } + + [Fact] + public async Task ShouldBootstrapLegacyWebProgramAndStartup() + { + using var client = Host.GetTestClient(); + + var response = await client.GetAsync("/").ConfigureAwait(false); + var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("Bootstrapper Web|Development", body); + Assert.Equal("Bootstrapper Web", Host.Services.GetRequiredService().Value); + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/ClassicWebApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/ClassicWebApplicationTestTest.cs new file mode 100644 index 0000000..f3d5de8 --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/ClassicWebApplicationTestTest.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.TestHost; +using Xunit; +using Classic = Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App.Program; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; + +public class ClassicWebApplicationTestTest : WebApplicationTest> +{ + public ClassicWebApplicationTestTest(BlockingManagedWebApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + { + } + + [Fact] + public async Task ShouldBootstrapApplication_WhenEntryPointExposesCreateHostBuilder() + { + using var client = Host.GetTestClient(); + + var response = await client.GetAsync("/").ConfigureAwait(false); + var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("Classic Program", body); + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests.csproj b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests.csproj new file mode 100644 index 0000000..3b29551 --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests.csproj @@ -0,0 +1,16 @@ + + + + net10.0;net9.0 + Codebelt.Extensions.Xunit.Hosting.AspNetCore + + + + + + + + + + + diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/DeferredInvalidWebApplicationTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/DeferredInvalidWebApplicationTest.cs new file mode 100644 index 0000000..72d51e6 --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/DeferredInvalidWebApplicationTest.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; + +internal sealed class DeferredInvalidWebApplicationTest : WebApplicationTest> +{ + public DeferredInvalidWebApplicationTest(BlockingManagedWebApplicationFixture hostFixture) : base(true, hostFixture) + { + } + + public void Configure(IWebHostBuilder builder) + { + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/DeferredModernWebApplicationTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/DeferredModernWebApplicationTest.cs new file mode 100644 index 0000000..de2174a --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/DeferredModernWebApplicationTest.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Hosting; +using ModernProgram = Codebelt.Extensions.Xunit.Hosting.Program.App.Program; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; + +internal sealed class DeferredModernWebApplicationTest : WebApplicationTest> +{ + public DeferredModernWebApplicationTest(BlockingManagedWebApplicationFixture hostFixture) : base(true, hostFixture) + { + } + + public void Configure(IWebHostBuilder builder) + { + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/WebApplicationTestFactoryTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/WebApplicationTestFactoryTest.cs new file mode 100644 index 0000000..56bb5be --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/WebApplicationTestFactoryTest.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Codebelt.Extensions.Xunit.Hosting.Program.App; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using BootstrapperMinimalWebProgram = Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App.Program; +using BootstrapperWebProgram = Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App.Program; +using Classic = Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App.Program; +using ModernProgram = Codebelt.Extensions.Xunit.Hosting.Program.App.Program; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; + +public class WebApplicationTestFactoryTest : Test +{ + public WebApplicationTestFactoryTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task Create_ShouldBootstrapApplication_WhenEntryPointUsesBootstrapperMinimalWebProgram() + { + using var application = WebApplicationTestFactory.Create(); + using var client = application.Host.GetTestClient(); + + var response = await client.GetAsync("/").ConfigureAwait(false); + var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("Bootstrapper Minimal Web", body); + } + + [Fact] + public async Task Create_ShouldBootstrapApplication_WhenEntryPointUsesBootstrapperWebProgram() + { + using var application = WebApplicationTestFactory.Create(); + using var client = application.Host.GetTestClient(); + + var response = await client.GetAsync("/").ConfigureAwait(false); + var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("Bootstrapper Web|Development", body); + } + + [Fact] + public async Task Create_ShouldBootstrapApplication_WhenEntryPointUsesClassicProgram() + { + using var application = WebApplicationTestFactory.Create(); + using var client = application.Host.GetTestClient(); + + var response = await client.GetAsync("/").ConfigureAwait(false); + var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("Classic Program", body); + } + + [Fact] + public async Task Create_ShouldBootstrapApplication_WhenEntryPointUsesModernProgramPattern() + { + using var application = WebApplicationTestFactory.Create(); + using var client = application.Host.GetTestClient(); + + var response = await client.GetAsync("/").ConfigureAwait(false); + var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("Modern Program|Development", body); + } + + [Fact] + public async Task Create_ShouldApplyWebHostConfiguration_WhenWebHostSetupIsProvided() + { + using var application = WebApplicationTestFactory.Create(builder => + { + builder.ConfigureAppConfiguration((_, configuration) => + { + configuration.AddInMemoryCollection(new Dictionary + { + ["ProgramLane:Message"] = "Configured from WebApplicationTestFactory" + }); + }); + + builder.ConfigureServices(services => + { + services.AddSingleton(new ProgramCustomization("Custom service from WebApplicationTestFactory")); + }); + }); + using var client = application.Host.GetTestClient(); + + var configurationResponse = await client.GetAsync("/configuration").ConfigureAwait(false); + var configurationBody = await configurationResponse.Content.ReadAsStringAsync().ConfigureAwait(false); + var serviceResponse = await client.GetAsync("/custom-service").ConfigureAwait(false); + var serviceBody = await serviceResponse.Content.ReadAsStringAsync().ConfigureAwait(false); + + Assert.True(configurationResponse.IsSuccessStatusCode); + Assert.Equal("Configured from WebApplicationTestFactory", configurationBody); + Assert.True(serviceResponse.IsSuccessStatusCode); + Assert.Equal("Custom service from WebApplicationTestFactory", serviceBody); + } + + [Fact] + public async Task RunAsync_ShouldReturnResponse_WhenEntryPointUsesModernProgramPattern() + { + using var response = await WebApplicationTestFactory.RunAsync().ConfigureAwait(false); + var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("Modern Program|Development", body); + } + + [Fact] + public void Create_CallerTypeShouldHaveDeclaringTypeOfWebApplicationTestFactoryTest() + { + Type sut = GetType(); + using var application = WebApplicationTestFactory.Create(_ => + { + }); + + Assert.True(sut == application.CallerType.DeclaringType); + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/WebApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/WebApplicationTestTest.cs new file mode 100644 index 0000000..7d2fb9d --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/WebApplicationTestTest.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Codebelt.Extensions.Xunit.Hosting.Program.App; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using ModernProgram = Codebelt.Extensions.Xunit.Hosting.Program.App.Program; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; + +public class WebApplicationTestTest : WebApplicationTest> +{ + public WebApplicationTestTest(BlockingManagedWebApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + { + } + + [Fact] + public async Task ShouldBootstrapApplication_WhenEntryPointUsesModernProgramPattern() + { + using var client = Host.GetTestClient(); + + var response = await client.GetAsync("/").ConfigureAwait(false); + var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("Modern Program|Development", body); + } + + [Fact] + public async Task ShouldApplyWebHostConfiguration_WhenConfigureWebHostIsOverridden() + { + using var client = Host.GetTestClient(); + + var response = await client.GetAsync("/configuration").ConfigureAwait(false); + var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("Configured from WebApplicationTest", body); + } + + [Fact] + public async Task ShouldAddServices_WhenConfigureWebHostIsOverridden() + { + using var client = Server.CreateClient(); + + var response = await client.GetAsync("/custom-service").ConfigureAwait(false); + var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("Custom service", body); + } + + [Fact] + public void ShouldExposeHostConfigurationEnvironmentAndServer() + { + Assert.NotNull(Host); + Assert.NotNull(Configuration); + Assert.NotNull(Environment); + Assert.NotNull(Server); + Assert.NotNull(Server.Services.GetRequiredService()); + Assert.Equal("Development", Environment.EnvironmentName); + } + + [Fact] + public void ShouldHaveValidFixtureState_WhenApplicationIsBootstrapped() + { + var fixture = new BlockingManagedWebApplicationFixture(); + var test = new DeferredModernWebApplicationTest(fixture); + + fixture.ConfigureCallback = test.Configure; + fixture.ConfigureWebHostCallback = test.Configure; + fixture.ConfigureHost(test); + + Assert.True(fixture.HasValidState()); + } + + [Fact] + public void Test_VerifyAbstractions() + { + Assert.IsAssignableFrom(this); + Assert.IsAssignableFrom(this); + Assert.IsAssignableFrom(this); + Assert.IsAssignableFrom(this); + Assert.IsAssignableFrom(this); + Assert.IsAssignableFrom(this); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureAppConfiguration((_, configuration) => + { + configuration.AddInMemoryCollection(new Dictionary + { + ["ProgramLane:Message"] = "Configured from WebApplicationTest" + }); + }); + + builder.ConfigureServices(services => + { + services.AddSingleton(new ProgramCustomization("Custom service")); + }); + } +} + diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests.csproj b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests.csproj index ff5a065..7a8c104 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests.csproj +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests.csproj @@ -10,8 +10,8 @@ - - - + + + diff --git a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/ApplicationTestFactoryTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/ApplicationTestFactoryTest.cs new file mode 100644 index 0000000..d9eab74 --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/ApplicationTestFactoryTest.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; +using BootstrapperConsoleMarker = Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App.BootstrapperConsoleMarker; +using BootstrapperConsoleProgram = Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App.Program; +using BootstrapperMinimalConsoleProgram = Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App.Program; +using BootstrapperMinimalWorkerMarker = Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App.BootstrapperMinimalWorkerMarker; +using BootstrapperMinimalWorkerProgram = Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App.Program; +using BootstrapperWorkerMarker = Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App.BootstrapperWorkerMarker; +using BootstrapperWorkerProgram = Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App.Program; + +namespace Codebelt.Extensions.Xunit.Hosting; + +public class ApplicationTestFactoryTest : Test +{ + public ApplicationTestFactoryTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Create_ShouldBootstrapApplication_WhenEntryPointUsesBootstrapperConsoleProgram() + { + using var application = ApplicationTestFactory.Create(); + + var marker = application.Host.Services.GetRequiredService(); + + Assert.Equal("Bootstrapper Console", marker.Value); + Assert.Equal("Development", application.Environment.EnvironmentName); + Assert.NotNull(application.Host); + } + + [Fact] + public void Create_ShouldBootstrapApplication_WhenEntryPointUsesMinimalConsoleProgram() + { + using var application = ApplicationTestFactory.Create(); + + Assert.Equal("Development", application.Environment.EnvironmentName); + Assert.NotNull(application.Host); + } + + [Fact] + public void Create_ShouldBootstrapApplication_WhenEntryPointUsesBootstrapperMinimalWorkerProgram() + { + using var application = ApplicationTestFactory.Create(); + + var marker = application.Host.Services.GetRequiredService(); + + Assert.Equal("Bootstrapper Minimal Worker", marker.Value); + Assert.Equal("Development", application.Environment.EnvironmentName); + Assert.NotNull(application.Host.Services.GetRequiredService()); + } + + [Fact] + public void Create_ShouldBootstrapApplication_WhenEntryPointUsesBootstrapperWorkerProgram() + { + using var application = ApplicationTestFactory.Create(); + + var marker = application.Host.Services.GetRequiredService(); + + Assert.Equal("Bootstrapper Worker", marker.Value); + Assert.Equal("Development", application.Environment.EnvironmentName); + Assert.NotNull(application.Host.Services.GetRequiredService()); + } + + [Fact] + public void Create_ShouldApplyHostConfiguration_WhenHostSetupIsProvided() + { + using var application = ApplicationTestFactory.Create(host => + { + host.ConfigureAppConfiguration((_, configuration) => + { + configuration.AddInMemoryCollection(new Dictionary + { + ["Factory:Message"] = "Configured from ApplicationTestFactory" + }); + }); + }); + + Assert.Equal("Configured from ApplicationTestFactory", application.Configuration["Factory:Message"]); + } + + [Fact] + public void Create_CallerTypeShouldHaveDeclaringTypeOfApplicationTestFactoryTest() + { + Type sut = GetType(); + using (var application = ApplicationTestFactory.Create(_ => + { + })) + { + Assert.True(sut == application.CallerType.DeclaringType); + } + } + + [Fact] + public async Task Create_ShouldDisposeFixtureAsync_WhenApplicationIsDisposedAsync() + { + await using var application = ApplicationTestFactory.Create(); + + Assert.NotNull(application.Host); + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperConsoleApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperConsoleApplicationTestTest.cs new file mode 100644 index 0000000..b7558ca --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperConsoleApplicationTestTest.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using BootstrapperConsoleMarker = Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App.BootstrapperConsoleMarker; +using BootstrapperConsoleProgram = Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App.Program; + +namespace Codebelt.Extensions.Xunit.Hosting; + +public class BootstrapperConsoleApplicationTestTest : ApplicationTest> +{ + public BootstrapperConsoleApplicationTestTest(BlockingManagedApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + { + } + + [Fact] + public void ShouldBootstrapLegacyConsoleProgramAndStartup() + { + var marker = Host.Services.GetRequiredService(); + Assert.Equal("Bootstrapper Console", marker.Value); + Assert.Equal("Development", Environment.EnvironmentName); + Assert.NotNull(Host); + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalConsoleApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalConsoleApplicationTestTest.cs new file mode 100644 index 0000000..4e7322c --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalConsoleApplicationTestTest.cs @@ -0,0 +1,23 @@ +using System; +using Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using BootstrapperMinimalConsoleProgram = Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App.Program; + +namespace Codebelt.Extensions.Xunit.Hosting; + +public class BootstrapperMinimalConsoleApplicationTestTest : ApplicationTest> +{ + public BootstrapperMinimalConsoleApplicationTestTest(BlockingManagedApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + { + } + + [Fact] + public void ShouldBootstrapMinimalConsoleProgram() + { + var marker = Host.Services.GetRequiredService(); + Assert.Equal("Bootstrapper Minimal Console", marker.Value); + Assert.Equal("Development", Environment.EnvironmentName); + Assert.NotNull(Host); + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalWorkerApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalWorkerApplicationTestTest.cs new file mode 100644 index 0000000..c566167 --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalWorkerApplicationTestTest.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; +using BootstrapperMinimalWorkerMarker = Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App.BootstrapperMinimalWorkerMarker; +using BootstrapperMinimalWorkerProgram = Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App.Program; + +namespace Codebelt.Extensions.Xunit.Hosting; + +public class BootstrapperMinimalWorkerApplicationTestTest : ApplicationTest> +{ + public BootstrapperMinimalWorkerApplicationTestTest(BlockingManagedApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + { + } + + [Fact] + public void ShouldBootstrapMinimalWorkerProgram() + { + var marker = Host.Services.GetRequiredService(); + + Assert.Equal("Bootstrapper Minimal Worker", marker.Value); + Assert.Equal("Development", Environment.EnvironmentName); + Assert.NotNull(Host.Services.GetRequiredService()); + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperWorkerApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperWorkerApplicationTestTest.cs new file mode 100644 index 0000000..349e9f7 --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperWorkerApplicationTestTest.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; +using BootstrapperWorkerMarker = Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App.BootstrapperWorkerMarker; +using BootstrapperWorkerProgram = Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App.Program; + +namespace Codebelt.Extensions.Xunit.Hosting; + +public class BootstrapperWorkerApplicationTestTest : ApplicationTest> +{ + public BootstrapperWorkerApplicationTestTest(BlockingManagedApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + { + } + + [Fact] + public void ShouldBootstrapLegacyWorkerProgramAndStartup() + { + var marker = Host.Services.GetRequiredService(); + + Assert.Equal("Bootstrapper Worker", marker.Value); + Assert.Equal("Development", Environment.EnvironmentName); + Assert.NotNull(Host.Services.GetRequiredService()); + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/Codebelt.Extensions.Xunit.Hosting.FunctionalTests.csproj b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/Codebelt.Extensions.Xunit.Hosting.FunctionalTests.csproj new file mode 100644 index 0000000..45198ca --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/Codebelt.Extensions.Xunit.Hosting.FunctionalTests.csproj @@ -0,0 +1,16 @@ + + + + net10.0;net9.0 + Codebelt.Extensions.Xunit.Hosting + + + + + + + + + + + diff --git a/test/Codebelt.Extensions.Xunit.Tests/TestTest.cs b/test/Codebelt.Extensions.Xunit.Tests/TestTest.cs index c835b09..ca5e7e1 100644 --- a/test/Codebelt.Extensions.Xunit.Tests/TestTest.cs +++ b/test/Codebelt.Extensions.Xunit.Tests/TestTest.cs @@ -64,7 +64,7 @@ public void Match_ShouldReturnTrue_WhenExactMatch() var expected = "Hello World"; var actual = "Hello World"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.True(result); } @@ -75,7 +75,7 @@ public void Match_ShouldReturnFalse_WhenNoMatch() var expected = "Hello World"; var actual = "Goodbye World"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.False(result); } @@ -86,7 +86,7 @@ public void Match_ShouldReturnTrue_WhenUsingGroupOfCharactersWildcard() var expected = "Hello * World"; var actual = "Hello Beautiful World"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.True(result); } @@ -97,7 +97,7 @@ public void Match_ShouldReturnTrue_WhenUsingSingleCharacterWildcard() var expected = "Hello ?orld"; var actual = "Hello World"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.True(result); } @@ -108,7 +108,7 @@ public void Match_ShouldReturnTrue_WhenUsingMultipleWildcards() var expected = "* test ? result *"; var actual = "This is a test 1 result with more text"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.True(result); } @@ -119,7 +119,7 @@ public void Match_ShouldReturnTrue_WhenUsingGroupOfCharactersWildcardAtStart() var expected = "*World"; var actual = "Hello World"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.True(result); } @@ -130,7 +130,7 @@ public void Match_ShouldReturnTrue_WhenUsingGroupOfCharactersWildcardAtEnd() var expected = "Hello*"; var actual = "Hello World"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.True(result); } @@ -141,7 +141,7 @@ public void Match_ShouldReturnFalse_WhenWildcardPatternDoesNotMatch() var expected = "Hello ? World"; var actual = "Hello Beautiful World"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.False(result); } @@ -153,7 +153,7 @@ public void Match_ShouldThrowArgumentOutOfRangeException_WhenThrowOnNoMatchIsTru var actual = "Goodbye World"; var exception = Assert.Throws(() => - Test.Match(expected, actual, options => options.ThrowOnNoMatch = true)); + Match(expected, actual, options => options.ThrowOnNoMatch = true)); TestOutput.WriteLine(exception.Message); TestOutput.WriteLine($"ParamName: {exception.ParamName}"); @@ -170,7 +170,7 @@ public void Match_ShouldReturnTrue_WithCustomGroupOfCharacters() var expected = "Hello ## World"; var actual = "Hello Beautiful World"; - var result = Test.Match(expected, actual, options => options.GroupOfCharacters = "\\#\\#"); + var result = Match(expected, actual, options => options.GroupOfCharacters = "\\#\\#"); Assert.True(result); } @@ -181,7 +181,7 @@ public void Match_ShouldReturnTrue_WithCustomSingleCharacter() var expected = "Hello #orld"; var actual = "Hello World"; - var result = Test.Match(expected, actual, options => options.SingleCharacter = "\\#"); + var result = Match(expected, actual, options => options.SingleCharacter = "\\#"); Assert.True(result); } @@ -192,7 +192,7 @@ public void Match_ShouldReturnTrue_WhenMatchingMultilineStrings() var expected = $"Line 1{Environment.NewLine}Line 2{Environment.NewLine}Line 3"; var actual = $"Line 1{Environment.NewLine}Line 2{Environment.NewLine}Line 3"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.True(result); } @@ -203,7 +203,7 @@ public void Match_ShouldReturnTrue_WhenMatchingMultilineStringsWithWildcards() var expected = $"Line 1{Environment.NewLine}Line ?{Environment.NewLine}Line *"; var actual = $"Line 1{Environment.NewLine}Line 2{Environment.NewLine}Line 3"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.True(result); } @@ -214,7 +214,7 @@ public void Match_ShouldReturnFalse_WhenMultilineStringsDoNotMatch() var expected = $"Line 1{Environment.NewLine}Line 2{Environment.NewLine}Line 3"; var actual = $"Line 1{Environment.NewLine}Line 4{Environment.NewLine}Line 3"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.False(result); } @@ -225,7 +225,7 @@ public void Match_ShouldReturnTrue_WhenMatchingEmptyStrings() var expected = string.Empty; var actual = string.Empty; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.True(result); } @@ -236,7 +236,7 @@ public void Match_ShouldReturnTrue_WhenMatchingSpecialRegexCharacters() var expected = "Hello.World"; var actual = "Hello.World"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.True(result); } @@ -247,7 +247,7 @@ public void Match_ShouldReturnTrue_WhenMatchingWithParentheses() var expected = "Test (value)"; var actual = "Test (value)"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.True(result); } @@ -258,7 +258,7 @@ public void Match_ShouldReturnTrue_WhenMatchingWithSquareBrackets() var expected = "Test [value]"; var actual = "Test [value]"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.True(result); } @@ -269,7 +269,7 @@ public void Match_ShouldReturnTrue_WhenMatchingWithPlusSign() var expected = "Test+Value"; var actual = "Test+Value"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.True(result); } @@ -280,7 +280,7 @@ public void Match_ShouldReturnFalse_WhenSingleCharacterWildcardMatchesMultipleCh var expected = "Test?Value"; var actual = "TestMultipleValue"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.False(result); } @@ -291,7 +291,7 @@ public void Match_ShouldReturnTrue_WhenGroupOfCharactersWildcardMatchesZeroChara var expected = "Test*Value"; var actual = "TestValue"; - var result = Test.Match(expected, actual); + var result = Match(expected, actual); Assert.True(result); } @@ -303,7 +303,7 @@ public void Match_ShouldThrowArgumentOutOfRangeException_WithNonMatchingLines() var actual = $"Line 1{Environment.NewLine}Actual Line{Environment.NewLine}Line 3"; var exception = Assert.Throws(() => - Test.Match(expected, actual, options => options.ThrowOnNoMatch = true)); + Match(expected, actual, options => options.ThrowOnNoMatch = true)); Assert.Contains("Expected Line", exception.ActualValue.ToString()); }