From 108ba792509bad0a185cb05281a40502d653643e Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 01:46:05 +0200 Subject: [PATCH 01/22] =?UTF-8?q?=E2=9C=A8=20add=20ApplicationHostFactory?= =?UTF-8?q?=20and=20hosting=20test=20base=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core abstractions for Program.cs-based host integration testing: - ApplicationHostFactory for creating started IHost instances from entry points - ApplicationTest{TEntryPoint,T} base class for host testing patterns - IApplicationFixture{TEntryPoint} for fixture-based lifecycle management - ManagedApplicationFixture{TEntryPoint} implementing host factory integration - ApplicationFixtureExtensions for convenient fixture setup methods - ProgramHostFactoryResolver for resolving host factories from entry points - DeferredHostBuilder for deferred host configuration Includes functional test coverage validating hosting abstractions. --- .../ApplicationFixtureExtensions.cs | 24 +++ .../ApplicationHostFactory.cs | 76 ++++++++ .../ApplicationTest.cs | 61 ++++++ .../IApplicationFixture.cs | 25 +++ .../Internal/DeferredHostBuilder.cs | 150 +++++++++++++++ .../Internal/ProgramHostFactoryResolver.cs | 179 ++++++++++++++++++ .../ManagedApplicationFixture.cs | 56 ++++++ .../BootstrapperConsoleApplicationTestTest.cs | 20 ++ .../BootstrapperEntryPointTest.cs | 1 + ...rapperMinimalConsoleApplicationTestTest.cs | 20 ++ ...trapperMinimalWorkerApplicationTestTest.cs | 24 +++ .../BootstrapperWorkerApplicationTestTest.cs | 24 +++ ...sions.Xunit.Hosting.FunctionalTests.csproj | 16 ++ 13 files changed, 676 insertions(+) create mode 100644 src/Codebelt.Extensions.Xunit.Hosting/ApplicationFixtureExtensions.cs create mode 100644 src/Codebelt.Extensions.Xunit.Hosting/ApplicationHostFactory.cs create mode 100644 src/Codebelt.Extensions.Xunit.Hosting/ApplicationTest.cs create mode 100644 src/Codebelt.Extensions.Xunit.Hosting/IApplicationFixture.cs create mode 100644 src/Codebelt.Extensions.Xunit.Hosting/Internal/DeferredHostBuilder.cs create mode 100644 src/Codebelt.Extensions.Xunit.Hosting/Internal/ProgramHostFactoryResolver.cs create mode 100644 src/Codebelt.Extensions.Xunit.Hosting/ManagedApplicationFixture.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperConsoleApplicationTestTest.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperEntryPointTest.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalConsoleApplicationTestTest.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalWorkerApplicationTestTest.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperWorkerApplicationTestTest.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/Codebelt.Extensions.Xunit.Hosting.FunctionalTests.csproj 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..a7322cb --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting/ApplicationHostFactory.cs @@ -0,0 +1,76 @@ +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 + { + 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, false, 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(); + } + + host.Start(); + 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/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/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..985ca23 --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting/Internal/ProgramHostFactoryResolver.cs @@ -0,0 +1,179 @@ +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 (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/src/Codebelt.Extensions.Xunit.Hosting/ManagedApplicationFixture.cs b/src/Codebelt.Extensions.Xunit.Hosting/ManagedApplicationFixture.cs new file mode 100644 index 0000000..5020dcf --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting/ManagedApplicationFixture.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Extensions.Xunit.Hosting; + +/// +/// Provides a default implementation of the interface. +/// +/// A type in the entry point assembly of the application. +/// +/// +public class ManagedApplicationFixture : HostFixture, IApplicationFixture where TEntryPoint : class +{ + /// + /// Initializes a new instance of the class. + /// + public ManagedApplicationFixture() + { + } + + /// + /// 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); + } + + /// + /// 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/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperConsoleApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperConsoleApplicationTestTest.cs new file mode 100644 index 0000000..2825e92 --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperConsoleApplicationTestTest.cs @@ -0,0 +1,20 @@ +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(ManagedApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + { + } + + [Fact] + public void ShouldBootstrapLegacyConsoleProgramAndStartup() + { + Assert.Equal("Bootstrapper Console", BootstrapperConsoleMarker.LastValue); + Assert.Equal("Development", Environment.EnvironmentName); + Assert.NotNull(Host); + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperEntryPointTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperEntryPointTest.cs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperEntryPointTest.cs @@ -0,0 +1 @@ + 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..549a14b --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalConsoleApplicationTestTest.cs @@ -0,0 +1,20 @@ +using Xunit; +using BootstrapperMinimalConsoleMarker = Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App.BootstrapperMinimalConsoleMarker; +using BootstrapperMinimalConsoleProgram = Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App.Program; + +namespace Codebelt.Extensions.Xunit.Hosting; + +public class BootstrapperMinimalConsoleApplicationTestTest : ApplicationTest> +{ + public BootstrapperMinimalConsoleApplicationTestTest(ManagedApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + { + } + + [Fact] + public void ShouldBootstrapMinimalConsoleProgram() + { + Assert.Equal("Bootstrapper Minimal Console", BootstrapperMinimalConsoleMarker.LastValue); + 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..6309c8b --- /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(ManagedApplicationFixture 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..edeaf5a --- /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(ManagedApplicationFixture 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 + + + + + + + + + + + From b0d4ed2aa93f8202d15da071d4bc13296bab4101 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 01:46:15 +0200 Subject: [PATCH 02/22] =?UTF-8?q?=E2=9C=A8=20add=20WebApplicationTest=20an?= =?UTF-8?q?d=20web=20application=20fixture=20abstractions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ASP.NET Core specific abstractions for Program.cs-based TestServer testing: - WebApplicationTest{TEntryPoint,T} base class for web application testing - IWebApplicationFixture{TEntryPoint} for web application lifecycle fixtures - ManagedWebApplicationFixture{TEntryPoint} implementing web fixture integration - WebApplicationFixtureExtensions for convenient web fixture setup methods - WebApplicationHostFactory for creating started web application instances Includes functional test coverage for ASP.NET Core Minimal Hosting and classic Startup.cs patterns with TestServer integration. --- .../IWebApplicationFixture.cs | 32 ++++++ .../Internal/WebApplicationHostFactory.cs | 18 +++ .../ManagedWebApplicationFixture.cs | 62 ++++++++++ .../WebApplicationFixtureExtensions.cs | 24 ++++ .../WebApplicationTest.cs | 65 +++++++++++ .../BootstrapperEntryPointTest.cs | 1 + ...otstrapperMinimalWebApplicationTestTest.cs | 27 +++++ .../BootstrapperWebApplicationTestTest.cs | 28 +++++ .../ClassicWebApplicationTestTest.cs | 25 ++++ ....Hosting.AspNetCore.FunctionalTests.csproj | 16 +++ .../DeferredInvalidWebApplicationTest.cs | 14 +++ .../DeferredModernWebApplicationTest.cs | 15 +++ .../ManagedWebApplicationFixtureTest.cs | 42 +++++++ .../WebApplicationTestTest.cs | 107 ++++++++++++++++++ 14 files changed, 476 insertions(+) create mode 100644 src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/IWebApplicationFixture.cs create mode 100644 src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Internal/WebApplicationHostFactory.cs create mode 100644 src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/ManagedWebApplicationFixture.cs create mode 100644 src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/WebApplicationFixtureExtensions.cs create mode 100644 src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/WebApplicationTest.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperEntryPointTest.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperMinimalWebApplicationTestTest.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperWebApplicationTestTest.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/ClassicWebApplicationTestTest.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests.csproj create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/DeferredInvalidWebApplicationTest.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/DeferredModernWebApplicationTest.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/ManagedWebApplicationFixtureTest.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/WebApplicationTestTest.cs 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..49b83b4 --- /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); + })); + } +} diff --git a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/ManagedWebApplicationFixture.cs b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/ManagedWebApplicationFixture.cs new file mode 100644 index 0000000..b1845b5 --- /dev/null +++ b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/ManagedWebApplicationFixture.cs @@ -0,0 +1,62 @@ +using System; +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; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; + +/// +/// Provides a default implementation of the interface. +/// +/// A type in the entry point assembly of the application. +/// +/// +public class ManagedWebApplicationFixture : HostFixture, IWebApplicationFixture where TEntryPoint : class +{ + /// + /// Initializes a new instance of the class. + /// + public ManagedWebApplicationFixture() + { + } + + /// + /// 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); + } + + /// + /// 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/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/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperEntryPointTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperEntryPointTest.cs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperEntryPointTest.cs @@ -0,0 +1 @@ + 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..8f52549 --- /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(ManagedWebApplicationFixture 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..84b0341 --- /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(ManagedWebApplicationFixture 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..164cd87 --- /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(ManagedWebApplicationFixture 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..19381fc --- /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(ManagedWebApplicationFixture 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..0836f7c --- /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(ManagedWebApplicationFixture hostFixture) : base(true, hostFixture) + { + } + + public void Configure(IWebHostBuilder builder) + { + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/ManagedWebApplicationFixtureTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/ManagedWebApplicationFixtureTest.cs new file mode 100644 index 0000000..ca653ef --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/ManagedWebApplicationFixtureTest.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 ManagedWebApplicationFixtureTest : Test +{ + public ManagedWebApplicationFixtureTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void ConfigureHost_ShouldThrowArgumentOutOfRangeException_WhenHostTestIsNotWebApplicationTest() + { + var fixture = new ManagedWebApplicationFixture(); + + var ex = Assert.Throws(() => fixture.ConfigureHost(this)); + + Assert.Equal("hostTest", ex.ParamName); + } + + [Fact] + public void ConfigureHost_ShouldThrowInvalidOperationException_WhenEntryPointAssemblyHasNoHost() + { + var fixture = new ManagedWebApplicationFixture(); + 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 ManagedWebApplicationFixture(); + + Assert.False(fixture.HasValidState()); + } +} 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..2950334 --- /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(ManagedWebApplicationFixture 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 ManagedWebApplicationFixture(); + 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")); + }); + } +} + From 3e7c395912fad154989c99667415ddde832ceec8 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 01:46:24 +0200 Subject: [PATCH 03/22] =?UTF-8?q?=F0=9F=A7=AA=20add=20bootstrapper=20refer?= =?UTF-8?q?ence=20applications?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight example applications demonstrating host patterns for testing: Generic Host patterns: - BootstrapperConsole.App with classic Startup.cs pattern - BootstrapperMinimalConsole.App with minimal hosting API - BootstrapperWorker.App with BackgroundService and Startup.cs - BootstrapperMinimalWorker.App minimal worker service pattern ASP.NET Core patterns: - BootstrapperWeb.App with ASP.NET Core and Startup.cs - BootstrapperMinimalWeb.App minimal ASP.NET Core pattern - BootstrapperClassicProgram.App top-level statement example - BootstrapperProgram.App advanced Program.cs customization Reference implementations for integration test validation. --- .../BootstrapperConsoleMarker.cs | 13 ++++++++ ...nit.Hosting.BootstrapperConsole.App.csproj | 13 ++++++++ .../Program.cs | 13 ++++++++ .../Startup.cs | 31 +++++++++++++++++ .../BootstrapperMinimalConsoleMarker.cs | 13 ++++++++ ...ting.BootstrapperMinimalConsole.App.csproj | 13 ++++++++ .../Program.cs | 26 +++++++++++++++ .../BootstrapperMinimalWebMarker.cs | 11 +++++++ ....Hosting.BootstrapperMinimalWeb.App.csproj | 13 ++++++++ .../Program.cs | 21 ++++++++++++ .../Properties/launchSettings.json | 12 +++++++ .../BootstrapperMinimalWorkerMarker.cs | 11 +++++++ .../BootstrapperMinimalWorkerService.cs | 13 ++++++++ ...sting.BootstrapperMinimalWorker.App.csproj | 13 ++++++++ .../Program.cs | 19 +++++++++++ .../BootstrapperWebMarker.cs | 11 +++++++ ...s.Xunit.Hosting.BootstrapperWeb.App.csproj | 13 ++++++++ .../Program.cs | 12 +++++++ .../Properties/launchSettings.json | 12 +++++++ .../Startup.cs | 33 +++++++++++++++++++ .../BootstrapperWorkerMarker.cs | 11 +++++++ .../BootstrapperWorkerService.cs | 13 ++++++++ ...unit.Hosting.BootstrapperWorker.App.csproj | 13 ++++++++ .../Program.cs | 13 ++++++++ .../Startup.cs | 19 +++++++++++ .../ClassicProgramMarker.cs | 11 +++++++ ...ns.Xunit.Hosting.ClassicProgram.App.csproj | 9 +++++ .../Program.cs | 29 ++++++++++++++++ .../Properties/launchSettings.json | 12 +++++++ ...xtensions.Xunit.Hosting.Program.App.csproj | 9 +++++ .../Program.cs | 33 +++++++++++++++++++ .../ProgramCustomization.cs | 11 +++++++ .../ProgramMarker.cs | 11 +++++++ .../Properties/launchSettings.json | 12 +++++++ 34 files changed, 522 insertions(+) create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/BootstrapperConsoleMarker.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App.csproj create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Program.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Startup.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/BootstrapperMinimalConsoleMarker.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App.csproj create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/Program.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/BootstrapperMinimalWebMarker.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App.csproj create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/Program.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/Properties/launchSettings.json create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/BootstrapperMinimalWorkerMarker.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/BootstrapperMinimalWorkerService.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App.csproj create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/Program.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/BootstrapperWebMarker.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App.csproj create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Program.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Properties/launchSettings.json create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Startup.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/BootstrapperWorkerMarker.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/BootstrapperWorkerService.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App.csproj create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/Program.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/Startup.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/ClassicProgramMarker.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App.csproj create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/Program.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/Properties/launchSettings.json create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.Program.App/Codebelt.Extensions.Xunit.Hosting.Program.App.csproj create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.Program.App/Program.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.Program.App/ProgramCustomization.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.Program.App/ProgramMarker.cs create mode 100644 app/Codebelt.Extensions.Xunit.Hosting.Program.App/Properties/launchSettings.json 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..47163ab --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/BootstrapperConsoleMarker.cs @@ -0,0 +1,13 @@ +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App; + +public sealed class BootstrapperConsoleMarker +{ + public BootstrapperConsoleMarker(string value) + { + Value = value; + } + + public string Value { get; } + + public static string LastValue { get; set; } +} 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..57a82d8 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App.csproj @@ -0,0 +1,13 @@ + + + + net10.0;net9.0 + Exe + 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..2b84cc1 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Startup.cs @@ -0,0 +1,31 @@ +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) + { + BootstrapperConsoleMarker.LastValue = serviceProvider.GetRequiredService().Value; + 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..58b4810 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/BootstrapperMinimalConsoleMarker.cs @@ -0,0 +1,13 @@ +namespace Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App; + +public sealed class BootstrapperMinimalConsoleMarker +{ + public BootstrapperMinimalConsoleMarker(string value) + { + Value = value; + } + + public string Value { get; } + + public static string LastValue { get; set; } +} 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..57a82d8 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App.csproj @@ -0,0 +1,13 @@ + + + + net10.0;net9.0 + Exe + 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..e9d1c85 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/Program.cs @@ -0,0 +1,26 @@ +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) + { + BootstrapperMinimalConsoleMarker.LastValue = serviceProvider.GetRequiredService().Value; + 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..b75a9d1 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWeb.App.csproj @@ -0,0 +1,13 @@ + + + + net10.0;net9.0 + false + Exe + + + + + + + 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..00e3828 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalWorker.App.csproj @@ -0,0 +1,13 @@ + + + + net10.0;net9.0 + Exe + 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..b75a9d1 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperWeb.App.csproj @@ -0,0 +1,13 @@ + + + + net10.0;net9.0 + false + Exe + + + + + + + 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..00e3828 --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperWorker.App.csproj @@ -0,0 +1,13 @@ + + + + net10.0;net9.0 + Exe + 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..9c97c7e --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App/Codebelt.Extensions.Xunit.Hosting.ClassicProgram.App.csproj @@ -0,0 +1,9 @@ + + + + net10.0;net9.0 + false + Exe + + + 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..9c97c7e --- /dev/null +++ b/app/Codebelt.Extensions.Xunit.Hosting.Program.App/Codebelt.Extensions.Xunit.Hosting.Program.App.csproj @@ -0,0 +1,9 @@ + + + + net10.0;net9.0 + false + Exe + + + 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 From 8edd09677efffe04752a7fea3ceafe120227a79a Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 01:46:35 +0200 Subject: [PATCH 04/22] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20upgrade=20dependenci?= =?UTF-8?q?es=20to=20latest=20compatible=20versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added new bootstrapper packages for integration test references: - Codebelt.Bootstrapper.Console (5.1.0) - Codebelt.Bootstrapper.Web (5.1.0) - Codebelt.Bootstrapper.Worker (5.1.0) Updated existing packages: - Codebelt.Extensions.BenchmarkDotNet.Console: 1.2.6 → 1.2.7 - Microsoft.NET.Test.Sdk: 18.5.1 → 18.6.0 All target framework versions remain compatible. --- Directory.Packages.props | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 + From 2657340e19c7300bf883ef3863775e98cf4d8371 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 01:46:43 +0200 Subject: [PATCH 05/22] =?UTF-8?q?=F0=9F=94=A7=20update=20solution=20and=20?= =?UTF-8?q?project=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Solution structure: - Added /app/ folder containing 8 bootstrapper reference applications - Added Codebelt.Extensions.Xunit.Hosting.FunctionalTests to /test/ folder - Added Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests to /test/ folder Project updates: - Updated Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests.csproj with new test references --- Codebelt.Extensions.Xunit.slnx | 12 ++++++++++++ ....Extensions.Xunit.Hosting.AspNetCore.Tests.csproj | 6 +++--- 2 files changed, 15 insertions(+), 3 deletions(-) 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/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 @@ - - - + + + From 9cb9961dfc9bd155e2a6b52d25ff61670aa60f27 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 01:46:54 +0200 Subject: [PATCH 06/22] =?UTF-8?q?=F0=9F=92=AC=20document=2011.1.0=20releas?= =?UTF-8?q?e=20for=20hosting=20packages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codebelt.Extensions.Xunit.Hosting 11.1.0: - Added ApplicationHostFactory for IHost creation from entry points - Added ApplicationTest{TEntryPoint,T} and related fixture abstractions - Updated version and availability across supported frameworks - Preserved existing version history with formatting cleanup Codebelt.Extensions.Xunit.Hosting.AspNetCore 11.1.0: - Added WebApplicationTest{TEntryPoint,T} and fixture abstractions - Added web application lifecycle and TestServer integration support - Updated version for .NET 10 and .NET 9 availability - Preserved existing version history with formatting cleanup --- .../PackageReleaseNotes.txt | 19 +++++++++++++----- .../PackageReleaseNotes.txt | 20 ++++++++++++++----- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/.nuget/Codebelt.Extensions.Xunit.Hosting.AspNetCore/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.Xunit.Hosting.AspNetCore/PackageReleaseNotes.txt index 6f1f329..82d8c15 100644 --- a/.nuget/Codebelt.Extensions.Xunit.Hosting.AspNetCore/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.Xunit.Hosting.AspNetCore/PackageReleaseNotes.txt @@ -1,12 +1,21 @@ -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 WebApplicationTest{TEntryPoint,T}, IWebApplicationFixture{TEntryPoint}, ManagedWebApplicationFixture{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..f3f8cbb 100644 --- a/.nuget/Codebelt.Extensions.Xunit.Hosting/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.Xunit.Hosting/PackageReleaseNotes.txt @@ -1,12 +1,22 @@ -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 ApplicationTest{TEntryPoint,T}, IApplicationFixture{TEntryPoint}, ManagedApplicationFixture{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) From b93af3167089e6397bd89e12adde6b26e00e47b8 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Fri, 5 Jun 2026 01:56:01 +0200 Subject: [PATCH 07/22] =?UTF-8?q?=F0=9F=92=AC=20add=2011.1.0=20release=20n?= =?UTF-8?q?otes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release documentation for version 11.1.0 including changelog updates and per-package release notes for Codebelt.Extensions.Xunit and Codebelt.Extensions.Xunit.App packages. --- .../PackageReleaseNotes.txt | 6 +++ .../PackageReleaseNotes.txt | 46 +++++++++++-------- CHANGELOG.md | 31 ++++++++++++- 3 files changed, 62 insertions(+), 21 deletions(-) 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/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/CHANGELOG.md b/CHANGELOG.md index 494ae73..fc4f557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,32 @@ 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 focused on Program.cs-based integration testing patterns, hosting abstractions for vanilla applications, and comprehensive fixture support for both Generic Host and ASP.NET Core 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, +- IApplicationFixture{TEntryPoint} interface and ManagedApplicationFixture{TEntryPoint} implementation in the Codebelt.Extensions.Xunit.Hosting namespace for fixture-based host lifecycle management, +- ApplicationFixtureExtensions class in the Codebelt.Extensions.Xunit.Hosting namespace providing convenient fixture setup methods, +- DeferredHostBuilder class in the Codebelt.Extensions.Xunit.Hosting namespace for deferred host configuration during fixture initialization, +- ProgramHostFactoryResolver class in the Codebelt.Extensions.Xunit.Hosting namespace for resolving host factories from entry points via reflection, +- WebApplicationTest{TEntryPoint,T} base classes in the Codebelt.Extensions.Xunit.Hosting.AspNetCore namespace for ASP.NET Core Program.cs-based TestServer testing, +- IWebApplicationFixture{TEntryPoint} interface and ManagedWebApplicationFixture{TEntryPoint} implementation in the Codebelt.Extensions.Xunit.Hosting.AspNetCore namespace for web application fixture lifecycle management, +- WebApplicationFixtureExtensions class in the Codebelt.Extensions.Xunit.Hosting.AspNetCore namespace providing convenient web fixture setup methods, +- WebApplicationHostFactory class in the Codebelt.Extensions.Xunit.Hosting.AspNetCore namespace for creating started web application instances from entry points, +- 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 both Generic Host and ASP.NET Core scenarios. + +### 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 +408,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 From ce46188d8a9c082187c3b8b5601df8cfa78b2ebd Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 04:37:54 +0200 Subject: [PATCH 08/22] =?UTF-8?q?=F0=9F=92=A5=20remove=20legacy=20managed?= =?UTF-8?q?=20fixture=20implementations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced by BlockingManagedApplicationFixture and BlockingManagedWebApplicationFixture as part of the factory-based testing pattern overhaul. The new approach centralizes test lifecycle management through factory methods (ApplicationTestFactory and WebApplicationTestFactory) rather than inheritance-based fixtures. --- .../ManagedWebApplicationFixture.cs | 62 ------------------- .../ManagedApplicationFixture.cs | 56 ----------------- 2 files changed, 118 deletions(-) delete mode 100644 src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/ManagedWebApplicationFixture.cs delete mode 100644 src/Codebelt.Extensions.Xunit.Hosting/ManagedApplicationFixture.cs diff --git a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/ManagedWebApplicationFixture.cs b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/ManagedWebApplicationFixture.cs deleted file mode 100644 index b1845b5..0000000 --- a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/ManagedWebApplicationFixture.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -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; - -namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; - -/// -/// Provides a default implementation of the interface. -/// -/// A type in the entry point assembly of the application. -/// -/// -public class ManagedWebApplicationFixture : HostFixture, IWebApplicationFixture where TEntryPoint : class -{ - /// - /// Initializes a new instance of the class. - /// - public ManagedWebApplicationFixture() - { - } - - /// - /// 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); - } - - /// - /// 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/ManagedApplicationFixture.cs b/src/Codebelt.Extensions.Xunit.Hosting/ManagedApplicationFixture.cs deleted file mode 100644 index 5020dcf..0000000 --- a/src/Codebelt.Extensions.Xunit.Hosting/ManagedApplicationFixture.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Codebelt.Extensions.Xunit.Hosting; - -/// -/// Provides a default implementation of the interface. -/// -/// A type in the entry point assembly of the application. -/// -/// -public class ManagedApplicationFixture : HostFixture, IApplicationFixture where TEntryPoint : class -{ - /// - /// Initializes a new instance of the class. - /// - public ManagedApplicationFixture() - { - } - - /// - /// 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); - } - - /// - /// 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; } -} From c2862bccc05a3eb88637fa6f632c3ff0b937a767 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 04:38:05 +0200 Subject: [PATCH 09/22] =?UTF-8?q?=E2=9C=A8=20introduce=20factory-based=20t?= =?UTF-8?q?esting=20for=20generic=20host?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ApplicationTestFactory static factory method for creating IHostTest instances from Program.cs entry points. Implement BlockingManagedApplicationFixture for non-blocking host fixture lifecycle management. Add internal ApplicationTest base class supporting host integration testing patterns with optional IApplicationFixture implementation override. Modify ApplicationHostFactory to remove automatic host.Start() call, allowing tests to control startup timing and resource initialization through the new factory pattern. --- .../ApplicationHostFactory.cs | 1 - .../ApplicationTestFactory.cs | 22 ++++++ .../BlockingManagedApplicationFixture.cs | 69 +++++++++++++++++++ .../Internal/ApplicationTest.cs | 47 +++++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 src/Codebelt.Extensions.Xunit.Hosting/ApplicationTestFactory.cs create mode 100644 src/Codebelt.Extensions.Xunit.Hosting/BlockingManagedApplicationFixture.cs create mode 100644 src/Codebelt.Extensions.Xunit.Hosting/Internal/ApplicationTest.cs diff --git a/src/Codebelt.Extensions.Xunit.Hosting/ApplicationHostFactory.cs b/src/Codebelt.Extensions.Xunit.Hosting/ApplicationHostFactory.cs index a7322cb..2e1bfb7 100644 --- a/src/Codebelt.Extensions.Xunit.Hosting/ApplicationHostFactory.cs +++ b/src/Codebelt.Extensions.Xunit.Hosting/ApplicationHostFactory.cs @@ -70,7 +70,6 @@ private static IHost BuildHost(IHostBuilder hostBuilder, Action co disposable.Dispose(); } - host.Start(); return host; } } 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/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); + } +} From 016fcb14e4358f7811a7ccf3c502501fb8cdc268 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 04:38:48 +0200 Subject: [PATCH 10/22] =?UTF-8?q?=E2=9C=A8=20introduce=20factory-based=20t?= =?UTF-8?q?esting=20for=20ASP.NET=20Core?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add WebApplicationTestFactory static factory method for creating IWebApplicationTest instances from Program.cs entry points. Implement BlockingManagedWebApplicationFixture for non-blocking web fixture lifecycle management. Add internal WebApplicationTest base class supporting ASP.NET Core TestServer integration testing patterns with optional IWebApplicationFixture implementation override. These additions provide WebApplicationFactory-like testing capabilities aligned with the Generic Host factory pattern introduced for consistency across the entire .NET application stack. --- .../BlockingManagedWebApplicationFixture.cs | 76 +++++++++++++++++++ .../Internal/WebApplicationTest.cs | 48 ++++++++++++ .../WebApplicationTestFactory.cs | 40 ++++++++++ 3 files changed, 164 insertions(+) create mode 100644 src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/BlockingManagedWebApplicationFixture.cs create mode 100644 src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Internal/WebApplicationTest.cs create mode 100644 src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/WebApplicationTestFactory.cs 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/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/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); + } +} From 2ec0d198dff4dbbc708ee521f0ac9bdf604d6b90 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 04:38:58 +0200 Subject: [PATCH 11/22] =?UTF-8?q?=F0=9F=94=A5=20remove=20obsolete=20bootst?= =?UTF-8?q?rapper=20entry=20point=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove BootstrapperEntryPointTest.cs from both FunctionalTests projects and ManagedWebApplicationFixtureTest.cs from AspNetCore.FunctionalTests. These tests were specific to now-removed fixture implementations and entry point resolver patterns. New factory-based test coverage through ApplicationTestFactoryTest, WebApplicationTestFactoryTest, and updated bootstrapper-specific test classes provides comprehensive replacement coverage. --- .../BootstrapperEntryPointTest.cs | 1 - .../ManagedWebApplicationFixtureTest.cs | 42 ------------------- .../BootstrapperEntryPointTest.cs | 1 - 3 files changed, 44 deletions(-) delete mode 100644 test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperEntryPointTest.cs delete mode 100644 test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/ManagedWebApplicationFixtureTest.cs delete mode 100644 test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperEntryPointTest.cs diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperEntryPointTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperEntryPointTest.cs deleted file mode 100644 index 8b13789..0000000 --- a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperEntryPointTest.cs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/ManagedWebApplicationFixtureTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/ManagedWebApplicationFixtureTest.cs deleted file mode 100644 index ca653ef..0000000 --- a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/ManagedWebApplicationFixtureTest.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using Xunit; -using ModernProgram = Codebelt.Extensions.Xunit.Hosting.Program.App.Program; - -namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; - -public class ManagedWebApplicationFixtureTest : Test -{ - public ManagedWebApplicationFixtureTest(ITestOutputHelper output) : base(output) - { - } - - [Fact] - public void ConfigureHost_ShouldThrowArgumentOutOfRangeException_WhenHostTestIsNotWebApplicationTest() - { - var fixture = new ManagedWebApplicationFixture(); - - var ex = Assert.Throws(() => fixture.ConfigureHost(this)); - - Assert.Equal("hostTest", ex.ParamName); - } - - [Fact] - public void ConfigureHost_ShouldThrowInvalidOperationException_WhenEntryPointAssemblyHasNoHost() - { - var fixture = new ManagedWebApplicationFixture(); - 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 ManagedWebApplicationFixture(); - - Assert.False(fixture.HasValidState()); - } -} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperEntryPointTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperEntryPointTest.cs deleted file mode 100644 index 8b13789..0000000 --- a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperEntryPointTest.cs +++ /dev/null @@ -1 +0,0 @@ - From 989d49c4ec80a972d455d338e68dd1b5ff0d5ca4 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 04:39:10 +0200 Subject: [PATCH 12/22] =?UTF-8?q?=E2=9C=85=20update=20hosting=20tests=20to?= =?UTF-8?q?=20use=20factory=20patterns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor all bootstrapper and integration test classes across FunctionalTests projects to adopt the new ApplicationTestFactory and WebApplicationTestFactory static factory methods. Update test initialization to remove dependency on inheritance-based fixture management and instead use the BlockingManagedApplicationFixture and BlockingManagedWebApplicationFixture implementations provided through factory methods. Modify TestTest.cs in unit tests to reflect updated Test base class signatures. These changes align all integration tests with the new standardized factory-based testing pattern. --- ...otstrapperMinimalWebApplicationTestTest.cs | 4 +- .../BootstrapperWebApplicationTestTest.cs | 4 +- .../ClassicWebApplicationTestTest.cs | 4 +- .../DeferredInvalidWebApplicationTest.cs | 4 +- .../DeferredModernWebApplicationTest.cs | 4 +- .../WebApplicationTestTest.cs | 6 +-- .../BootstrapperConsoleApplicationTestTest.cs | 8 ++-- ...rapperMinimalConsoleApplicationTestTest.cs | 15 ++++--- ...trapperMinimalWorkerApplicationTestTest.cs | 4 +- .../BootstrapperWorkerApplicationTestTest.cs | 4 +- .../TestTest.cs | 44 +++++++++---------- 11 files changed, 53 insertions(+), 48 deletions(-) diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperMinimalWebApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperMinimalWebApplicationTestTest.cs index 8f52549..c561553 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperMinimalWebApplicationTestTest.cs +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperMinimalWebApplicationTestTest.cs @@ -6,9 +6,9 @@ namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; -public class BootstrapperMinimalWebApplicationTestTest : WebApplicationTest> +public class BootstrapperMinimalWebApplicationTestTest : WebApplicationTest> { - public BootstrapperMinimalWebApplicationTestTest(ManagedWebApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + public BootstrapperMinimalWebApplicationTestTest(BlockingManagedWebApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) { } diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperWebApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperWebApplicationTestTest.cs index 84b0341..7dd7bc7 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperWebApplicationTestTest.cs +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BootstrapperWebApplicationTestTest.cs @@ -7,9 +7,9 @@ namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; -public class BootstrapperWebApplicationTestTest : WebApplicationTest> +public class BootstrapperWebApplicationTestTest : WebApplicationTest> { - public BootstrapperWebApplicationTestTest(ManagedWebApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + public BootstrapperWebApplicationTestTest(BlockingManagedWebApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) { } diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/ClassicWebApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/ClassicWebApplicationTestTest.cs index 164cd87..f3d5de8 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/ClassicWebApplicationTestTest.cs +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/ClassicWebApplicationTestTest.cs @@ -5,9 +5,9 @@ namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; -public class ClassicWebApplicationTestTest : WebApplicationTest> +public class ClassicWebApplicationTestTest : WebApplicationTest> { - public ClassicWebApplicationTestTest(ManagedWebApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + public ClassicWebApplicationTestTest(BlockingManagedWebApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) { } diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/DeferredInvalidWebApplicationTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/DeferredInvalidWebApplicationTest.cs index 19381fc..72d51e6 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/DeferredInvalidWebApplicationTest.cs +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/DeferredInvalidWebApplicationTest.cs @@ -2,9 +2,9 @@ namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; -internal sealed class DeferredInvalidWebApplicationTest : WebApplicationTest> +internal sealed class DeferredInvalidWebApplicationTest : WebApplicationTest> { - public DeferredInvalidWebApplicationTest(ManagedWebApplicationFixture hostFixture) : base(true, hostFixture) + public DeferredInvalidWebApplicationTest(BlockingManagedWebApplicationFixture hostFixture) : base(true, hostFixture) { } diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/DeferredModernWebApplicationTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/DeferredModernWebApplicationTest.cs index 0836f7c..de2174a 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/DeferredModernWebApplicationTest.cs +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/DeferredModernWebApplicationTest.cs @@ -3,9 +3,9 @@ namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; -internal sealed class DeferredModernWebApplicationTest : WebApplicationTest> +internal sealed class DeferredModernWebApplicationTest : WebApplicationTest> { - public DeferredModernWebApplicationTest(ManagedWebApplicationFixture hostFixture) : base(true, hostFixture) + public DeferredModernWebApplicationTest(BlockingManagedWebApplicationFixture hostFixture) : base(true, hostFixture) { } diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/WebApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/WebApplicationTestTest.cs index 2950334..7d2fb9d 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/WebApplicationTestTest.cs +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/WebApplicationTestTest.cs @@ -11,9 +11,9 @@ namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore; -public class WebApplicationTestTest : WebApplicationTest> +public class WebApplicationTestTest : WebApplicationTest> { - public WebApplicationTestTest(ManagedWebApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + public WebApplicationTestTest(BlockingManagedWebApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) { } @@ -67,7 +67,7 @@ public void ShouldExposeHostConfigurationEnvironmentAndServer() [Fact] public void ShouldHaveValidFixtureState_WhenApplicationIsBootstrapped() { - var fixture = new ManagedWebApplicationFixture(); + var fixture = new BlockingManagedWebApplicationFixture(); var test = new DeferredModernWebApplicationTest(fixture); fixture.ConfigureCallback = test.Configure; diff --git a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperConsoleApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperConsoleApplicationTestTest.cs index 2825e92..38de16e 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperConsoleApplicationTestTest.cs +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperConsoleApplicationTestTest.cs @@ -1,19 +1,21 @@ +using Microsoft.Testing.Platform.Services; 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 class BootstrapperConsoleApplicationTestTest : ApplicationTest> { - public BootstrapperConsoleApplicationTestTest(ManagedApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + public BootstrapperConsoleApplicationTestTest(BlockingManagedApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) { } [Fact] public void ShouldBootstrapLegacyConsoleProgramAndStartup() { - Assert.Equal("Bootstrapper Console", BootstrapperConsoleMarker.LastValue); + 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 index 549a14b..6aef184 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalConsoleApplicationTestTest.cs +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalConsoleApplicationTestTest.cs @@ -1,19 +1,22 @@ +using System; +using Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App; +using Microsoft.Testing.Platform.Services; using Xunit; -using BootstrapperMinimalConsoleMarker = Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App.BootstrapperMinimalConsoleMarker; using BootstrapperMinimalConsoleProgram = Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App.Program; namespace Codebelt.Extensions.Xunit.Hosting; -public class BootstrapperMinimalConsoleApplicationTestTest : ApplicationTest> +public class BootstrapperMinimalConsoleApplicationTestTest : ApplicationTest> { - public BootstrapperMinimalConsoleApplicationTestTest(ManagedApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) - { + public BootstrapperMinimalConsoleApplicationTestTest(BlockingManagedApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + { } [Fact] public void ShouldBootstrapMinimalConsoleProgram() - { - Assert.Equal("Bootstrapper Minimal Console", BootstrapperMinimalConsoleMarker.LastValue); + { + 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 index 6309c8b..c566167 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalWorkerApplicationTestTest.cs +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalWorkerApplicationTestTest.cs @@ -6,9 +6,9 @@ namespace Codebelt.Extensions.Xunit.Hosting; -public class BootstrapperMinimalWorkerApplicationTestTest : ApplicationTest> +public class BootstrapperMinimalWorkerApplicationTestTest : ApplicationTest> { - public BootstrapperMinimalWorkerApplicationTestTest(ManagedApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + public BootstrapperMinimalWorkerApplicationTestTest(BlockingManagedApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) { } diff --git a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperWorkerApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperWorkerApplicationTestTest.cs index edeaf5a..349e9f7 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperWorkerApplicationTestTest.cs +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperWorkerApplicationTestTest.cs @@ -6,9 +6,9 @@ namespace Codebelt.Extensions.Xunit.Hosting; -public class BootstrapperWorkerApplicationTestTest : ApplicationTest> +public class BootstrapperWorkerApplicationTestTest : ApplicationTest> { - public BootstrapperWorkerApplicationTestTest(ManagedApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + public BootstrapperWorkerApplicationTestTest(BlockingManagedApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) { } 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()); } From 6652e2a006478df58e6f104e97103787c8117905 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 04:39:21 +0200 Subject: [PATCH 13/22] =?UTF-8?q?=E2=9C=85=20add=20comprehensive=20test=20?= =?UTF-8?q?coverage=20for=20new=20factory=20patterns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ApplicationTestFactoryTest to FunctionalTests providing full coverage of ApplicationTestFactory method signatures, fixture overrides, and host lifecycle management. Add WebApplicationTestFactoryTest to AspNetCore.FunctionalTests validating WebApplicationTestFactory behavior and TestServer integration. Add BlockingManagedWebApplicationFixtureTest covering the new blocking web fixture implementation. These tests ensure the new factory pattern works reliably across Generic Host and ASP.NET Core scenarios with all bootstrapper configuration variations. --- ...lockingManagedWebApplicationFixtureTest.cs | 42 ++++++ .../WebApplicationTestFactoryTest.cs | 125 ++++++++++++++++++ .../ApplicationTestFactoryTest.cs | 96 ++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/BlockingManagedWebApplicationFixtureTest.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.FunctionalTests/WebApplicationTestFactoryTest.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/ApplicationTestFactoryTest.cs 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/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.FunctionalTests/ApplicationTestFactoryTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/ApplicationTestFactoryTest.cs new file mode 100644 index 0000000..f17d34a --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/ApplicationTestFactoryTest.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +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); + } + } +} From 2802916690f8fc29e3bc7b0ea484a526c8398da0 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 04:39:31 +0200 Subject: [PATCH 14/22] =?UTF-8?q?=F0=9F=93=9D=20update=20API=20namespace?= =?UTF-8?q?=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regenerate and update the official DocFX namespace pages for Codebelt.Extensions.Xunit.Hosting and Codebelt.Extensions.Xunit.Hosting.AspNetCore namespaces to document the new factory classes (ApplicationTestFactory, WebApplicationTestFactory), base classes (ApplicationTest, WebApplicationTest), and fixture implementations (BlockingManagedApplicationFixture, BlockingManagedWebApplicationFixture). Documentation pages serve as the authoritative source for public API conventions and are updated to reflect the factory-based testing pattern. --- ...elt.Extensions.Xunit.Hosting.AspNetCore.md | 26 ++++++++++++++----- .../Codebelt.Extensions.Xunit.Hosting.md | 24 ++++++++++++----- 2 files changed, 38 insertions(+), 12 deletions(-) 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`| From bf2763b633889a7fb58924401868c8a7a02a6c70 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 04:39:42 +0200 Subject: [PATCH 15/22] =?UTF-8?q?=F0=9F=93=9D=20update=20repo=20guidelines?= =?UTF-8?q?=20for=20API=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new 'Official Documentation' section to AGENTS.md establishing that public API conventions belong in .docfx/api/namespaces/ and should be treated as the authoritative documentation source for library behavior and naming vocabulary. Clarify that public APIs should have corresponding updates to the relevant namespace page when introducing or clarifying a convention. Emphasize that internal reasoning, exploratory notes, and agent discussion should be excluded from DocFX pages, which should contain only stable public guidance. --- AGENTS.md | 6 ++++++ 1 file changed, 6 insertions(+) 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.). From e9c603a2b83b0ac5100050daba36b97d0aee1875 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 04:39:53 +0200 Subject: [PATCH 16/22] =?UTF-8?q?=F0=9F=92=AC=20update=20release=20communi?= =?UTF-8?q?cation=20and=20package=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update CHANGELOG.md [11.1.0] entry to emphasize the release brings WebApplicationFactory-like integration testing patterns to the entire .NET application stack through ApplicationHostFactory and ApplicationTest abstractions for Generic Host, and WebApplicationTest for ASP.NET Core. Reorganize Added section to highlight ApplicationTestFactory and WebApplicationTestFactory along with BlockingManagedApplicationFixture and BlockingManagedWebApplicationFixture. Update per-assembly package release notes for both Hosting and AspNetCore namespaces with version 11.1.0 feature summaries and availability information. --- .../PackageReleaseNotes.txt | 3 ++- .../PackageReleaseNotes.txt | 3 ++- CHANGELOG.md | 15 ++++++++------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.nuget/Codebelt.Extensions.Xunit.Hosting.AspNetCore/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.Xunit.Hosting.AspNetCore/PackageReleaseNotes.txt index 82d8c15..172899f 100644 --- a/.nuget/Codebelt.Extensions.Xunit.Hosting.AspNetCore/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.Xunit.Hosting.AspNetCore/PackageReleaseNotes.txt @@ -5,7 +5,8 @@ Availability: .NET 10 and .NET 9 - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) # New Features -- ADDED WebApplicationTest{TEntryPoint,T}, IWebApplicationFixture{TEntryPoint}, ManagedWebApplicationFixture{TEntryPoint} and WebApplicationFixtureExtensions in the Codebelt.Extensions.Xunit.Hosting.AspNetCore namespace to support Program.cs based ASP.NET Core tests with TestServer +- 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 diff --git a/.nuget/Codebelt.Extensions.Xunit.Hosting/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.Xunit.Hosting/PackageReleaseNotes.txt index f3f8cbb..2d7d86e 100644 --- a/.nuget/Codebelt.Extensions.Xunit.Hosting/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.Xunit.Hosting/PackageReleaseNotes.txt @@ -6,7 +6,8 @@ Availability: .NET 10, .NET 9 and .NET Standard 2.0 # New Features - ADDED ApplicationHostFactory class in the Codebelt.Extensions.Xunit.Hosting namespace for creating started IHost instances from an application entry point assembly -- ADDED ApplicationTest{TEntryPoint,T}, IApplicationFixture{TEntryPoint}, ManagedApplicationFixture{TEntryPoint} and ApplicationFixtureExtensions in the Codebelt.Extensions.Xunit.Hosting namespace to support Program.cs based host tests for console, worker and generic host applications +- 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index fc4f557..a35bd85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,22 +9,23 @@ For more details, please refer to `PackageReleaseNotes.txt` on a per assembly ba ## [11.1.0] - 2026-06-05 -This is a minor release focused on Program.cs-based integration testing patterns, hosting abstractions for vanilla applications, and comprehensive fixture support for both Generic Host and ASP.NET Core scenarios. +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, -- IApplicationFixture{TEntryPoint} interface and ManagedApplicationFixture{TEntryPoint} implementation in the Codebelt.Extensions.Xunit.Hosting namespace for fixture-based host lifecycle management, +- 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, -- DeferredHostBuilder class in the Codebelt.Extensions.Xunit.Hosting namespace for deferred host configuration during fixture initialization, -- ProgramHostFactoryResolver class in the Codebelt.Extensions.Xunit.Hosting namespace for resolving host factories from entry points via reflection, - WebApplicationTest{TEntryPoint,T} base classes in the Codebelt.Extensions.Xunit.Hosting.AspNetCore namespace for ASP.NET Core Program.cs-based TestServer testing, -- IWebApplicationFixture{TEntryPoint} interface and ManagedWebApplicationFixture{TEntryPoint} implementation in the Codebelt.Extensions.Xunit.Hosting.AspNetCore namespace for web application fixture lifecycle management, +- 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, -- WebApplicationHostFactory class in the Codebelt.Extensions.Xunit.Hosting.AspNetCore namespace for creating started web application instances from entry points, - 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 both Generic Host and ASP.NET Core scenarios. +- Comprehensive functional test coverage for hosting abstractions and integration patterns across Generic Host and ASP.NET Core scenarios, including all bootstrapper configurations. ### Changed From d5e62bea5d1b47fbce85e42fd6357dfbc5c60f64 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 04:40:02 +0200 Subject: [PATCH 17/22] =?UTF-8?q?=F0=9F=99=88=20add=20generated=20assets?= =?UTF-8?q?=20to=20gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update .gitignore to exclude build outputs, generated artifacts, and temporary files that are created during the build and test process but should not be committed to version control. --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 From 28acb9024c5f2b973b6434245a3f4b4f1fe866d4 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 04:45:52 +0200 Subject: [PATCH 18/22] =?UTF-8?q?=F0=9F=91=B7=20add=20support=20for=20macO?= =?UTF-8?q?S=20tests=20in=20CI=20pipeline=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-pipeline.yml | 87 ++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 7 deletions(-) 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 }} From 4580460b479fdb190a1f79d15df916408107bfcf Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Fri, 5 Jun 2026 05:12:34 +0200 Subject: [PATCH 19/22] =?UTF-8?q?=E2=9C=A8=20support=20configurable=20appl?= =?UTF-8?q?ication=20stop=20behavior=20in=20host=20factory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add optional stopApplication parameter to ApplicationHostFactory.Create overload, allowing callers to control whether the entry point is stopped after the host is built. Updates ProgramHostFactoryResolver exception handling and WebApplicationHostFactory parameter passing to support this new configuration option. --- .../Internal/WebApplicationHostFactory.cs | 2 +- .../ApplicationHostFactory.cs | 17 ++++++++++++++++- .../Internal/ProgramHostFactoryResolver.cs | 3 +++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Internal/WebApplicationHostFactory.cs b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Internal/WebApplicationHostFactory.cs index 49b83b4..cd0528b 100644 --- a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Internal/WebApplicationHostFactory.cs +++ b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Internal/WebApplicationHostFactory.cs @@ -13,6 +13,6 @@ public static IHost Create(Action configureWebHost { webHostBuilder.UseTestServer(o => o.PreserveExecutionContext = true); configureWebHost?.Invoke(webHostBuilder); - })); + }), false); } } diff --git a/src/Codebelt.Extensions.Xunit.Hosting/ApplicationHostFactory.cs b/src/Codebelt.Extensions.Xunit.Hosting/ApplicationHostFactory.cs index 2e1bfb7..022048d 100644 --- a/src/Codebelt.Extensions.Xunit.Hosting/ApplicationHostFactory.cs +++ b/src/Codebelt.Extensions.Xunit.Hosting/ApplicationHostFactory.cs @@ -21,6 +21,21 @@ public static class ApplicationHostFactory /// 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()); @@ -42,7 +57,7 @@ public static IHost Create(Action configureHost) wher }); }); - var hostFactory = ProgramHostFactoryResolver.ResolveHostFactory(assembly, false, deferredHostBuilder.ConfigureHostBuilder, deferredHostBuilder.EntryPointCompleted); + 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."); diff --git a/src/Codebelt.Extensions.Xunit.Hosting/Internal/ProgramHostFactoryResolver.cs b/src/Codebelt.Extensions.Xunit.Hosting/Internal/ProgramHostFactoryResolver.cs index 985ca23..8d47fb7 100644 --- a/src/Codebelt.Extensions.Xunit.Hosting/Internal/ProgramHostFactoryResolver.cs +++ b/src/Codebelt.Extensions.Xunit.Hosting/Internal/ProgramHostFactoryResolver.cs @@ -155,6 +155,9 @@ private void InvokeEntryPoint() catch (TargetInvocationException ex) when (ex.InnerException?.GetType().Name == nameof(HostAbortedException)) { } + catch (HostAbortedException) + { + } catch (TargetInvocationException ex) { exception = ex.InnerException ?? ex; From 648c26a38ba5da12233d319ad5021dd670f0b4a8 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 5 Jun 2026 05:28:41 +0200 Subject: [PATCH 20/22] =?UTF-8?q?=F0=9F=94=A7=20disable=20analyzers=20and?= =?UTF-8?q?=20adjust=20warning=20levels=20in=20project=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...xtensions.Xunit.Hosting.BootstrapperConsole.App.csproj | 8 ++++++++ ...ns.Xunit.Hosting.BootstrapperMinimalConsole.App.csproj | 8 ++++++++ ...nsions.Xunit.Hosting.BootstrapperMinimalWeb.App.csproj | 8 ++++++++ ...ons.Xunit.Hosting.BootstrapperMinimalWorker.App.csproj | 8 ++++++++ ...lt.Extensions.Xunit.Hosting.BootstrapperWeb.App.csproj | 8 ++++++++ ...Extensions.Xunit.Hosting.BootstrapperWorker.App.csproj | 8 ++++++++ ...elt.Extensions.Xunit.Hosting.ClassicProgram.App.csproj | 8 ++++++++ .../Codebelt.Extensions.Xunit.Hosting.Program.App.csproj | 8 ++++++++ 8 files changed, 64 insertions(+) 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 index 57a82d8..84750d0 100644 --- 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 @@ -4,6 +4,14 @@ 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/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App.csproj b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App.csproj index 57a82d8..84750d0 100644 --- 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 @@ -4,6 +4,14 @@ net10.0;net9.0 Exe false + false + false + false + true + 0 + none + NU1701,NETSDK1206 + false 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 index b75a9d1..f3a06e4 100644 --- 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 @@ -4,6 +4,14 @@ net10.0;net9.0 false Exe + false + false + false + true + 0 + none + NU1701,NETSDK1206 + false 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 index 00e3828..2de2a06 100644 --- 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 @@ -4,6 +4,14 @@ net10.0;net9.0 Exe false + false + false + false + true + 0 + none + NU1701,NETSDK1206 + false 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 index b75a9d1..f3a06e4 100644 --- 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 @@ -4,6 +4,14 @@ net10.0;net9.0 false Exe + false + false + false + true + 0 + none + NU1701,NETSDK1206 + false 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 index 00e3828..2de2a06 100644 --- 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 @@ -4,6 +4,14 @@ net10.0;net9.0 Exe false + false + false + false + true + 0 + none + NU1701,NETSDK1206 + false 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 index 9c97c7e..61b3e41 100644 --- 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 @@ -4,6 +4,14 @@ 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/Codebelt.Extensions.Xunit.Hosting.Program.App.csproj b/app/Codebelt.Extensions.Xunit.Hosting.Program.App/Codebelt.Extensions.Xunit.Hosting.Program.App.csproj index 9c97c7e..61b3e41 100644 --- 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 @@ -4,6 +4,14 @@ net10.0;net9.0 false Exe + false + false + false + true + 0 + none + NU1701,NETSDK1206 + false From 076503f76f599a3e3be61db6e69fb7ce1996bf8a Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Fri, 5 Jun 2026 05:53:55 +0200 Subject: [PATCH 21/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Remove=20LastValue?= =?UTF-8?q?=20support=20from=20bootstrapper=20markers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplifies marker classes by removing the unused static state property that was only used for testing purposes. --- .../BootstrapperConsoleMarker.cs | 10 ++++------ .../Startup.cs | 1 - .../BootstrapperMinimalConsoleMarker.cs | 10 ++++------ .../Program.cs | 1 - .../BootstrapperConsoleApplicationTestTest.cs | 2 +- .../BootstrapperMinimalConsoleApplicationTestTest.cs | 10 +++++----- 6 files changed, 14 insertions(+), 20 deletions(-) diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/BootstrapperConsoleMarker.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/BootstrapperConsoleMarker.cs index 47163ab..dd6ac36 100644 --- a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/BootstrapperConsoleMarker.cs +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/BootstrapperConsoleMarker.cs @@ -5,9 +5,7 @@ public sealed class BootstrapperConsoleMarker public BootstrapperConsoleMarker(string value) { Value = value; - } - - public string Value { get; } - - public static string LastValue { get; set; } -} + } + + public string Value { get; } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Startup.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Startup.cs index 2b84cc1..ff8cb3c 100644 --- a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Startup.cs +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App/Startup.cs @@ -25,7 +25,6 @@ public override void ConfigureConsole(IServiceProvider serviceProvider) public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) { - BootstrapperConsoleMarker.LastValue = serviceProvider.GetRequiredService().Value; 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 index 58b4810..82dd16f 100644 --- a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/BootstrapperMinimalConsoleMarker.cs +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/BootstrapperMinimalConsoleMarker.cs @@ -5,9 +5,7 @@ public sealed class BootstrapperMinimalConsoleMarker public BootstrapperMinimalConsoleMarker(string value) { Value = value; - } - - public string Value { get; } - - public static string LastValue { get; set; } -} + } + + public string Value { get; } +} diff --git a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/Program.cs b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/Program.cs index e9d1c85..ab69e38 100644 --- a/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/Program.cs +++ b/app/Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App/Program.cs @@ -20,7 +20,6 @@ public static Task Main(string[] args) public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) { - BootstrapperMinimalConsoleMarker.LastValue = serviceProvider.GetRequiredService().Value; return Task.CompletedTask; } } diff --git a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperConsoleApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperConsoleApplicationTestTest.cs index 38de16e..b7558ca 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperConsoleApplicationTestTest.cs +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperConsoleApplicationTestTest.cs @@ -1,4 +1,4 @@ -using Microsoft.Testing.Platform.Services; +using Microsoft.Extensions.DependencyInjection; using Xunit; using BootstrapperConsoleMarker = Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App.BootstrapperConsoleMarker; using BootstrapperConsoleProgram = Codebelt.Extensions.Xunit.Hosting.BootstrapperConsole.App.Program; diff --git a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalConsoleApplicationTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalConsoleApplicationTestTest.cs index 6aef184..4e7322c 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalConsoleApplicationTestTest.cs +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/BootstrapperMinimalConsoleApplicationTestTest.cs @@ -1,8 +1,8 @@ -using System; -using Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App; -using Microsoft.Testing.Platform.Services; -using Xunit; -using BootstrapperMinimalConsoleProgram = Codebelt.Extensions.Xunit.Hosting.BootstrapperMinimalConsole.App.Program; +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; From c75ac31b9c65e9cf5b1749450a16f1a10f1ec3f2 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Fri, 5 Jun 2026 05:54:04 +0200 Subject: [PATCH 22/22] =?UTF-8?q?=E2=9C=85=20Add=20async=20disposal=20test?= =?UTF-8?q?=20for=20ApplicationTestFactory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verifies that ApplicationTestFactory properly disposes the fixture when the application is disposed asynchronously. --- .../ApplicationTestFactoryTest.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/ApplicationTestFactoryTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/ApplicationTestFactoryTest.cs index f17d34a..d9eab74 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/ApplicationTestFactoryTest.cs +++ b/test/Codebelt.Extensions.Xunit.Hosting.FunctionalTests/ApplicationTestFactoryTest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -93,4 +94,12 @@ public void Create_CallerTypeShouldHaveDeclaringTypeOfApplicationTestFactoryTest Assert.True(sut == application.CallerType.DeclaringType); } } + + [Fact] + public async Task Create_ShouldDisposeFixtureAsync_WhenApplicationIsDisposedAsync() + { + await using var application = ApplicationTestFactory.Create(); + + Assert.NotNull(application.Host); + } }