Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ Availability: .NET 9, .NET 8 and .NET Standard 2.0
# New Features
- ADDED HostTest class in the Codebelt.Extensions.Xunit.Hosting namespace that represents the non-generic base class from where its generic equivalent should derive (e.g., MinimalHostTest{T}, HostTest{T}, etc.)
- ADDED IGenericHostFixture interface in the Codebelt.Extensions.Xunit.Hosting namespace that provides a set of members for configuring the host
- ADDED GenericHostFixture class in the Codebelt.Extensions.Xunit.Hosting namespace that provides a default implementation of the IGenericHostFixture interface
- ADDED GenericHostFixture class in the Codebelt.Extensions.Xunit.Hosting namespace that provides a default implementation of the IGenericHostFixture interface (replaces the legacy HostFixture class)
- ADDED GenericHostFixtureExtensions class in the Codebelt.Extensions.Xunit.Hosting namespace that consist of one extension method for the IGenericHostFixture interface: HasValidState
- ADDED IMinimalHostFixture interface in the Codebelt.Extensions.Xunit.Hosting namespace that provides a set of members for configuring the host (minimal style)
- ADDED MinimalHostFixture class in the Codebelt.Extensions.Xunit.Hosting namespace that provides a default implementation of the IMinimalHostFixture interface
- ADDED MinimalHostFixtureExtensions class in the Codebelt.Extensions.Xunit.Hosting namespace that consist of one extension method for the IMinimalHostFixture interface: HasValidState
- ADDED MinimalHostTest class in the Codebelt.Extensions.Xunit.Hosting namespace that represents the non-generic base class from where its generic equivalent should derive (e.g., MinimalWebHostTest, {T}, MinimalHostTest{T}, etc.)
- ADDED MinimalHostTestFactory class in the Codebelt.Extensions.Xunit.Hosting namespace that provides a set of static methods for IHost unit testing (minimal style)
- EXTENDED ServiceCollectionExtensions class in the Codebelt.Extensions.Xunit.Hosting namespace with one new extension method for the IServiceCollection interface: An overload of AddXunitTestLogging that does not rely on the ITestOutputHelper interface

Version 9.1.3
Availability: .NET 9, .NET 8 and .NET Standard 2.0
Expand Down
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ For more details, please refer to `PackageReleaseNotes.txt` on a per assembly ba

## [10.0.0] - TBD

This major release introduces support for unit testing Minimal APIs and includes numerous breaking changes with valuable learnings from previous 9.0.x releases. These changes aim to ensure greater consistency across the `Codebelt.Extensions.Xunit.Hosting` and `Codebelt.Extensions.Xunit.Hosting.AspNetCore` namespaces.
This major release introduces support for unit testing Minimal APIs and includes numerous breaking changes with valuable learnings from previous 9.1.x releases. These changes aim to ensure greater consistency across the `Codebelt.Extensions.Xunit.Hosting` and `Codebelt.Extensions.Xunit.Hosting.AspNetCore` namespaces.

### Added

- HostTest class in the Codebelt.Extensions.Xunit.Hosting namespace that represents the non-generic base class from where its generic equivalent should derive (e.g., MinimalHostTest{T}, HostTest{T}, etc.)
- IGenericHostFixture interface in the Codebelt.Extensions.Xunit.Hosting namespace that provides a set of members for configuring the host
- GenericHostFixture class in the Codebelt.Extensions.Xunit.Hosting namespace that provides a default implementation of the IGenericHostFixture interface
- GenericHostFixture class in the Codebelt.Extensions.Xunit.Hosting namespace that provides a default implementation of the IGenericHostFixture interface (replaces the legacy HostFixture class)
- GenericHostFixtureExtensions class in the Codebelt.Extensions.Xunit.Hosting namespace that consist of one extension method for the IGenericHostFixture interface: HasValidState
- IMinimalHostFixture interface in the Codebelt.Extensions.Xunit.Hosting namespace that provides a set of members for configuring the host (minimal style)
- MinimalHostFixture class in the Codebelt.Extensions.Xunit.Hosting namespace that provides a default implementation of the IMinimalHostFixture interface
Expand All @@ -31,6 +31,8 @@ This major release introduces support for unit testing Minimal APIs and includes
- MinimalHostFixtureExtensions class in the Codebelt.Extensions.Xunit.Hosting namespace that consist of one extension method for the IMinimalHostFixture interface: HasValidState
- MinimalHostTest class in the Codebelt.Extensions.Xunit.Hosting namespace that represents the non-generic base class from where its generic equivalent should derive (e.g., MinimalWebHostTest, {T}, MinimalHostTest{T}, etc.)
- MinimalHostTestFactory class in the Codebelt.Extensions.Xunit.Hosting namespace that provides a set of static methods for IHost unit testing (minimal style)
- ServiceCollectionExtensions class in the Codebelt.Extensions.Xunit.Hosting namespace received one new extension method for the IServiceCollection interface: An overload of AddXunitTestLogging that does not rely on the ITestOutputHelper interface
- This was done to help mitigate those scenarios where the [current design of xUnit v2 and ITestOutputHelper can cause deadlocks](https://github.com/xunit/xunit/discussions/2994), e.g. you can access the xUnit logger but nothing is logged to the test output

### Changed

Expand Down
16 changes: 8 additions & 8 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<PackageVersion Include="Cuemon.Core" Version="9.0.3" />
<PackageVersion Include="Cuemon.Extensions.AspNetCore" Version="9.0.3" />
<PackageVersion Include="Cuemon.Extensions.IO" Version="9.0.3" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.3" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.4" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageVersion Include="MinVer" Version="6.0.0" />
<PackageVersion Include="NativeLibraryLoader" Version="1.0.13" />
Expand All @@ -24,19 +24,19 @@
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith('net9'))">
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="9.0.4" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith('net8')) OR $(TargetFramework.StartsWith('netstandard2'))">
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="8.0.14" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="8.0.15" />
</ItemGroup>
</Project>
4 changes: 2 additions & 2 deletions src/Codebelt.Extensions.Xunit.Hosting/LoggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
public static class LoggerExtensions
{
/// <summary>
/// Returns the associated <see cref="ITestStore{T}"/> that is provided when settings up services from <see cref="ServiceCollectionExtensions.AddXunitTestLogging"/>.
/// Returns the associated <see cref="ITestStore{T}"/> that is provided when settings up services from <see cref="ServiceCollectionExtensions.AddXunitTestLogging(Microsoft.Extensions.DependencyInjection.IServiceCollection,Microsoft.Extensions.Logging.LogLevel)"/> or related.
/// </summary>
/// <param name="logger">The <see cref="ILogger{TCategoryName}"/> from which to retrieve the <see cref="ITestStore{T}"/>.</param>
/// <param name="categoryName">The category name for messages produced by the <paramref name="logger"/> -or- <c>null</c> for messages produced by all loggers.</param>
Expand All @@ -27,7 +27,7 @@
/// <exception cref="KeyNotFoundException">
/// <paramref name="logger"/> does not contain a test store for the specified <paramref name="categoryName"/>.
/// </exception>
public static ITestStore<XunitTestLoggerEntry> GetTestStore(this ILogger logger, string categoryName = null)

Check warning on line 30 in src/Codebelt.Extensions.Xunit.Hosting/LoggerExtensions.cs

View workflow job for this annotation

GitHub Actions / call-sonarcloud / 🔬 Code Quality Analysis

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 30 in src/Codebelt.Extensions.Xunit.Hosting/LoggerExtensions.cs

View workflow job for this annotation

GitHub Actions / call-sonarcloud / 🔬 Code Quality Analysis

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 30 in src/Codebelt.Extensions.Xunit.Hosting/LoggerExtensions.cs

View workflow job for this annotation

GitHub Actions / call-sonarcloud / 🔬 Code Quality Analysis

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 30 in src/Codebelt.Extensions.Xunit.Hosting/LoggerExtensions.cs

View workflow job for this annotation

GitHub Actions / call-sonarcloud / 🔬 Code Quality Analysis

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 30 in src/Codebelt.Extensions.Xunit.Hosting/LoggerExtensions.cs

View workflow job for this annotation

GitHub Actions / call-sonarcloud / 🔬 Code Quality Analysis

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 30 in src/Codebelt.Extensions.Xunit.Hosting/LoggerExtensions.cs

View workflow job for this annotation

GitHub Actions / call-sonarcloud / 🔬 Code Quality Analysis

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
if (logger == null) { throw new ArgumentNullException(nameof(logger)); }
var loggerType = logger.GetType();
Expand Down Expand Up @@ -57,7 +57,7 @@
}

/// <summary>
/// Returns the associated <see cref="ITestStore{T}"/> that is provided when settings up services from <see cref="ServiceCollectionExtensions.AddXunitTestLogging"/>.
/// Returns the associated <see cref="ITestStore{T}"/> that is provided when settings up services from <see cref="ServiceCollectionExtensions.AddXunitTestLogging(Microsoft.Extensions.DependencyInjection.IServiceCollection,Microsoft.Extensions.Logging.LogLevel)"/> or related.
/// </summary>
/// <param name="logger">The <see cref="ILogger{TCategoryName}"/> from which to retrieve the <see cref="ITestStore{T}"/>.</param>
/// <returns>Returns an implementation of <see cref="ITestStore{T}"/> with all logged entries expressed as <see cref="XunitTestLoggerEntry"/>.</returns>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,27 @@ namespace Codebelt.Extensions.Xunit.Hosting
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds a unit test optimized implementation of logging to the <paramref name="services"/> collection.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to extend.</param>
/// <param name="minimumLevel">The <see cref="LogLevel"/> that specifies the minimum level to include for the logging.</param>
/// <returns>A reference to <paramref name="services" /> so that additional configuration calls can be chained.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="services"/> cannot be null.
/// </exception>
public static IServiceCollection AddXunitTestLogging(this IServiceCollection services, LogLevel minimumLevel = LogLevel.Trace)
{
if (services == null) { throw new ArgumentNullException(nameof(services)); }
services.AddLogging(builder =>
{
builder.SetMinimumLevel(minimumLevel);
builder.AddProvider(new XunitTestLoggerProvider());
});

return services;
}

/// <summary>
/// Adds a unit test optimized implementation of output logging to the <paramref name="services"/> collection.
/// </summary>
Expand All @@ -22,34 +43,34 @@ public static class ServiceCollectionExtensions
/// <paramref name="services"/> cannot be null -or-
/// <paramref name="output"/> cannot be null.
/// </exception>
public static IServiceCollection AddXunitTestLogging(this IServiceCollection services, ITestOutputHelper output, LogLevel minimumLevel = LogLevel.Trace)
{
public static IServiceCollection AddXunitTestLogging(this IServiceCollection services, ITestOutputHelper output, LogLevel minimumLevel = LogLevel.Trace)
{
if (services == null) { throw new ArgumentNullException(nameof(services)); }
if (output == null) { throw new ArgumentNullException(nameof(output)); }
if (services.Any(sd => sd.ServiceType == typeof(ITestOutputHelperAccessor)))
{
services.AddLogging(builder =>
{
services.AddLogging(builder =>
{
builder.SetMinimumLevel(minimumLevel);
builder.Services.AddSingleton<ILoggerProvider>(provider =>
{
var accessor = provider.GetRequiredService<ITestOutputHelperAccessor>();
accessor.TestOutput = output;
return new XunitTestLoggerProvider(accessor);
});
});
});
}
else
{
services.AddLogging(builder =>
{
builder.SetMinimumLevel(minimumLevel);
builder.AddProvider(new XunitTestLoggerProvider(output));
});
services.AddLogging(builder =>
{
builder.SetMinimumLevel(minimumLevel);
builder.AddProvider(new XunitTestLoggerProvider(output));
});
}
return services;
}
return services;
}

/// <summary>
/// Adds a default implementation of <see cref="ITestOutputHelperAccessor"/> to the <paramref name="services"/> collection.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Codebelt.Extensions.Xunit.Hosting/XunitTestLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
}
else
{
_output.WriteLine(message);
_output?.WriteLine(message);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ internal sealed class XunitTestLoggerProvider : InMemoryTestStore<XunitTestLogge
private readonly ITestOutputHelperAccessor _accessor;
private readonly ITestOutputHelper _output;

public XunitTestLoggerProvider()
{
}

public XunitTestLoggerProvider(ITestOutputHelper output)
{
_output = output;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public ServiceCollectionExtensions(ITestOutputHelper output) : base(output)
public void AddXunitTestLogging_ShouldAddXunitTestLogging()
{
var services = new ServiceCollection();
services.AddXunitTestLogging(TestOutput);
services.AddXunitTestLogging();

var provider = services.BuildServiceProvider();

Expand Down Expand Up @@ -67,7 +67,61 @@ public void AddXunitTestLogging_ShouldAddXunitTestLogging()
entry => Assert.Equal("Information: SUT", entry.ToString()),
entry => Assert.Equal("Warning: SUT", entry.ToString()),
entry => Assert.Equal("Information: Unique message for logger2.", entry.ToString()));
}

[Fact]
public void AddXunitTestLogging_ShouldAddXunitTestLoggingWithTestOutput()
{
var services = new ServiceCollection();
services.AddXunitTestLogging(TestOutput);

var provider = services.BuildServiceProvider();

var logger1 = provider.GetRequiredService<ILogger<ServiceCollectionExtensions>>();
var loggerStore1 = logger1.GetTestStore();
var loggerStore1Duplicate = logger1.GetTestStore(typeof(ServiceCollectionExtensions).FullName!.ToLowerInvariant());

var logger2 = provider.GetRequiredService<ILogger<Test>>();
var loggerStore2 = logger2.GetTestStore(null); // all loggers

logger1.LogCritical("SUT");
logger1.LogTrace("SUT");
logger1.LogDebug("SUT");
logger1.LogError("SUT");
logger1.LogInformation("SUT");
logger1.LogWarning("SUT");

logger2.LogInformation("Unique message for logger2.");

Assert.Throws<KeyNotFoundException>(() => logger2.GetTestStore("InvalidValue"));

Assert.Equal(loggerStore1, loggerStore1Duplicate);

Assert.NotNull(logger1);
Assert.NotNull(loggerStore1);

Assert.NotNull(logger2);
Assert.NotNull(loggerStore2);

Assert.Equal(6, loggerStore1.Query().Count());
Assert.Equal(7, loggerStore2.Query().Count());

Assert.Collection(loggerStore1.Query(),
entry => Assert.Equal("Critical: SUT", entry.ToString()),
entry => Assert.Equal("Trace: SUT", entry.ToString()),
entry => Assert.Equal("Debug: SUT", entry.ToString()),
entry => Assert.Equal("Error: SUT", entry.ToString()),
entry => Assert.Equal("Information: SUT", entry.ToString()),
entry => Assert.Equal("Warning: SUT", entry.ToString()));

Assert.Collection(loggerStore2.Query(),
entry => Assert.Equal("Critical: SUT", entry.ToString()),
entry => Assert.Equal("Trace: SUT", entry.ToString()),
entry => Assert.Equal("Debug: SUT", entry.ToString()),
entry => Assert.Equal("Error: SUT", entry.ToString()),
entry => Assert.Equal("Information: SUT", entry.ToString()),
entry => Assert.Equal("Warning: SUT", entry.ToString()),
entry => Assert.Equal("Information: Unique message for logger2.", entry.ToString()));
}
}
}
2 changes: 1 addition & 1 deletion testenvironments.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
"name": "Docker-Ubuntu",
"type": "docker",
"dockerImage": "gimlichael/ubuntu-testrunner:mono-net8.0.407-9.0.202"
"dockerImage": "gimlichael/ubuntu-testrunner:mono-net8.0.408-9.0.203"
}
]
}
Loading