Skip to content

Commit

Permalink
Add testing loggers and extensions for xUnit.
Browse files Browse the repository at this point in the history
This commit introduces various logging infrastructure for testing purposes. It includes logging extensions for xUnit that create test logger instances and a test logger factory using the ITestOutputHelper from Xunit. Frank.PulseFlow has been used to send and handle logging output flow, improving the readability and organization of test logs.
  • Loading branch information
frankhaugen committed Jan 3, 2024
1 parent 1b4670d commit 627fb4d
Show file tree
Hide file tree
Showing 18 changed files with 450 additions and 22 deletions.
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
<PackageIcon>icon.png</PackageIcon>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://www.github.com/frankhaugen/Frank.TODO</PackageProjectUrl>
<PackageProjectUrl>https://www.github.com/frankhaugen/Frank.Testing.Logging</PackageProjectUrl>

<RepositoryUrl>https://www.github.com/frankhaugen/Frank.TODO</RepositoryUrl>
<RepositoryUrl>https://www.github.com/frankhaugen/Frank.Testing.Logging</RepositoryUrl>
<RepositoryType>git</RepositoryType>
</PropertyGroup>

Expand Down
17 changes: 17 additions & 0 deletions Frank.Testing.Logging/DictionaryExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Frank.Testing.Logging;

internal static class DictionaryExtensions
{
public static TValue GetOrCreate<TKey, TValue>(
this IDictionary<TKey, TValue> dictionary,
TKey key,
Func<TValue> valueFactory)
{
if (dictionary.TryGetValue(key, out TValue? value) && value != null)
return value;

value = valueFactory();
dictionary[key] = value;
return value;
}
}
19 changes: 19 additions & 0 deletions Frank.Testing.Logging/Frank.Testing.Logging.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>Loggers for the Microsoft.Extensions.Logging framework wrapping the xUnit.net ITestOutputHelper.</Description>
<PackageTags>xunit, test, testing, tests, runner, console, logger, logging, output, helper, itestoutputhelper, dependency, injection, di, ioc, microsoft, extensions, frank, haugen</PackageTags>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Frank.PulseFlow.Logging" Version="1.5.0" />
<PackageReference Include="Frank.Reflection" Version="1.0.0" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="xunit.extensibility.core" Version="2.6.4" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Frank.Testing.TestOutputExtensions\Frank.Testing.TestOutputExtensions.csproj" />
</ItemGroup>
</Project>
24 changes: 24 additions & 0 deletions Frank.Testing.Logging/LoggingBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Frank.PulseFlow;
using Frank.PulseFlow.Logging;

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Frank.Testing.Logging;

public static class LoggingBuilderExtensions
{
/// <summary>
/// Adds test logging to the ILoggingBuilder.
/// </summary>
/// <param name="builder">The ILoggingBuilder to add the test logging to.</param>
/// <param name="outputHelper">The ITestOutputHelper to redirect the logging output to.</param>

Check warning on line 15 in Frank.Testing.Logging/LoggingBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / Merge Job / Publish Preview Job

XML comment has a param tag for 'outputHelper', but there is no parameter by that name

Check warning on line 15 in Frank.Testing.Logging/LoggingBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / Merge Job / Publish Preview Job

XML comment has a param tag for 'outputHelper', but there is no parameter by that name
/// <param name="logLevel">The log level to use for the test logging. Default is LogLevel.Debug.</param>

Check warning on line 16 in Frank.Testing.Logging/LoggingBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / Merge Job / Publish Preview Job

XML comment has a param tag for 'logLevel', but there is no parameter by that name

Check warning on line 16 in Frank.Testing.Logging/LoggingBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / Merge Job / Publish Preview Job

XML comment has a param tag for 'logLevel', but there is no parameter by that name
/// <returns>The modified ILoggingBuilder with the test logging added.</returns>
public static ILoggingBuilder AddPulseFlowTestLoggingProvider(this ILoggingBuilder builder)
{
builder.AddPulseFlow();
builder.Services.AddSingleton<IFlow, TestLoggingOutputFlow>();
return builder;
}
}
16 changes: 16 additions & 0 deletions Frank.Testing.Logging/PulseFlowTestLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Frank.PulseFlow;
using Frank.PulseFlow.Logging;

using Microsoft.Extensions.Logging;

namespace Frank.Testing.Logging;

public class PulseFlowTestLogger(IConduit conduit) : ILogger
{
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
=> conduit.SendAsync(new LogPulse<TState>(logLevel, eventId, state, exception, formatter, "TestLogger")).GetAwaiter().GetResult();

public bool IsEnabled(LogLevel logLevel) => true;

public IDisposable? BeginScope<TState>(TState state) where TState : notnull => new PulseFlowLoggerScope<TState>(state);
}
22 changes: 22 additions & 0 deletions Frank.Testing.Logging/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

using Xunit.Abstractions;

namespace Frank.Testing.Logging;

public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds test logging to the IServiceCollection.
/// </summary>
/// <param name="services">The IServiceCollection to add the test logging to.</param>
/// <param name="outputHelper">The ITestOutputHelper to redirect the logging output to.</param>
/// <param name="logLevel">The log level to use for the test logging. Default is LogLevel.Debug.</param>
/// <returns>The modified IServiceCollection with the test logging added.</returns>
public static IServiceCollection AddTestLogging(this IServiceCollection services, ITestOutputHelper outputHelper, LogLevel logLevel = LogLevel.Debug)
{
services.AddLogging(builder => builder.AddPulseFlowTestLoggingProvider());
return services;
}
}
37 changes: 37 additions & 0 deletions Frank.Testing.Logging/TestLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Microsoft.Extensions.Logging;

using Xunit.Abstractions;

namespace Frank.Testing.Logging;

public class TestLogger<T> : ILogger<T>
{
public ITestOutputHelper OutputHelper { get; }
public LogLevel LogLevel { get; }
public string? CategoryName { get; }

public TestLogger(ITestOutputHelper outputHelper, LogLevel logLevel, string? categoryName = null)
{
OutputHelper = outputHelper;
LogLevel = logLevel;
CategoryName = categoryName;
}

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (logLevel < LogLevel)
return;

OutputHelper.WriteLine($"{logLevel}: {formatter(state, exception)}");
}

public bool IsEnabled(LogLevel logLevel)
{
return logLevel >= LogLevel;
}

public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
return new TestLoggerScope<TState>(state);
}
}
8 changes: 8 additions & 0 deletions Frank.Testing.Logging/TestLoggerScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Frank.Testing.Logging;

public class TestLoggerScope<TState>(TState state) : IDisposable
{
public TState? State { get; private set; } = state;

public void Dispose() => State = default;
}
20 changes: 20 additions & 0 deletions Frank.Testing.Logging/TestLoggingOutputFlow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Frank.PulseFlow;
using Frank.PulseFlow.Logging;

using Xunit.Abstractions;

namespace Frank.Testing.Logging;

/// <summary>
/// Represents a class that handles logging output flow.
/// </summary>
public class TestLoggingOutputFlow(ITestOutputHelper outputHelper) : IFlow
{
public Task HandleAsync(IPulse pulse, CancellationToken cancellationToken)
{
outputHelper.WriteLine(pulse.ToString());
return Task.CompletedTask;
}

public bool CanHandle(Type pulseType) => pulseType == typeof(LogPulse<>);
}
35 changes: 35 additions & 0 deletions Frank.Testing.Logging/TestOutputHelperExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Frank.PulseFlow.Logging;
using Frank.Reflection;

using Microsoft.Extensions.Logging;

using Xunit.Abstractions;

namespace Frank.Testing.Logging;

public static class TestOutputHelperExtensions
{
/// <summary>
/// Creates a test logger instance with the specified log category name.
/// </summary>
/// <typeparam name="T">The type of the class to which the created logger will be associated.</typeparam>
/// <param name="outputHelper">The ITestOutputHelper instance used for logging.</param>
/// <param name="categoryName">The name of the log category. (optional)</param>

Check warning on line 17 in Frank.Testing.Logging/TestOutputHelperExtensions.cs

View workflow job for this annotation

GitHub Actions / Merge Job / Publish Preview Job

XML comment has a param tag for 'categoryName', but there is no parameter by that name
/// <param name="logLevel"></param>
/// <returns>An instance of <see cref="ILogger{T}"/> that can be used for logging tests.</returns>
public static ILogger<T> CreateTestLogger<T>(this ITestOutputHelper outputHelper, LogLevel logLevel = LogLevel.Debug)
=> new TestLogger<T>(outputHelper, logLevel, typeof(T).GetDisplayName());

/// <summary>
/// Creates a test logger factory using the specified <paramref name="outputHelper"/>.
/// </summary>
/// <param name="outputHelper">The test output helper.</param>
/// <param name="logLevel"></param>
/// <returns>A new instance of <see cref="ILoggerFactory"/> for testing purposes.</returns>
public static ILoggerFactory CreateTestLoggerFactory(this ITestOutputHelper outputHelper, LogLevel logLevel = LogLevel.Debug)
{
var factory = new LoggerFactory();
// factory.AddProvider(new PulseFlowLoggerProvider());
return factory;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<RootNamespace>Xunit</RootNamespace>
<Description>Extends ITestOutputHelper to allow output of a generic type using a serializer.</Description>
<PackageTags>test, xunit, output, helper, serializer, json, console</PackageTags>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="xunit.abstractions" Version="2.0.3" />
<PackageReference Include="Xunit.Extensions.Ordering" Version="1.4.5" />
</ItemGroup>

</Project>
32 changes: 32 additions & 0 deletions Frank.Testing.TestOutputExtensions/TestOutputExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Text.Json;
using System.Text.Json.Serialization;

using Xunit.Abstractions;

namespace Xunit;

public static class TestOutputExtensions
{
/// <summary>
/// Writes the specified object's string representation followed by the current line terminator to the output.
/// </summary>
/// <typeparam name="T">The type of the object to write.</typeparam>
/// <param name="outputHelper">The ITestOutputHelper instance.</param>
/// <param name="source">The object to write.</param>
/// <exception cref="System.Text.Json.JsonException">Thrown when unable to serialize the object.</exception>
public static void WriteLine<T>(this ITestOutputHelper outputHelper, T? source) => outputHelper.WriteLine(JsonSerializer.Serialize(source, JsonSerializerOptions));

/// <summary>
/// Writes a JSON representation of the specified object to the output helper.
/// </summary>
/// <typeparam name="T">The type of object to be serialized.</typeparam>
/// <param name="outputHelper">The output helper to write the JSON to.</param>
/// <param name="source">The object to be serialized.</param>
/// <param name="options">The options to be used for the serialization (optional).</param>
/// <remarks>
/// If <paramref name="options"/> is not provided, the default <see cref="JsonSerializerOptions"/> will be used.
/// </remarks>
public static void WriteJson<T>(this ITestOutputHelper outputHelper, T? source, JsonSerializerOptions? options = null) => outputHelper.WriteLine(options == null ? JsonSerializer.Serialize(source, JsonSerializerOptions) : JsonSerializer.Serialize(source, options));

private static JsonSerializerOptions JsonSerializerOptions => new() { Converters = { new JsonStringEnumConverter() }, WriteIndented = true, ReferenceHandler = ReferenceHandler.IgnoreCycles, PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
}
30 changes: 30 additions & 0 deletions Frank.Testing.Tests/Frank.Testing.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.4" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Frank.Testing.Logging\Frank.Testing.Logging.csproj" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions Frank.Testing.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using Xunit;
35 changes: 35 additions & 0 deletions Frank.Testing.Tests/TestOutputExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using JetBrains.Annotations;

using Xunit.Abstractions;

namespace Frank.Testing.Tests;

[TestSubject(typeof(TestOutputExtensions))]
public class TestOutputExtensionsTests
{
private readonly ITestOutputHelper _outputHelper;

public TestOutputExtensionsTests(ITestOutputHelper outputHelper)
{
_outputHelper = outputHelper;
}

[Fact]
public void Test1()
{
var model = new TestModel { Name = "Frank" };
_outputHelper.WriteLine(model);
}

[Fact]
public void Test2()
{
var model = new TestModel { Name = "Frank" };
_outputHelper.WriteJson(model);
}

private class TestModel
{
public string? Name { get; set; }
}
}
Loading

0 comments on commit 627fb4d

Please sign in to comment.