diff --git a/docs/preview/03-Features/03-logging.mdx b/docs/preview/03-Features/03-logging.mdx
index 493e6f4f..c2d9c946 100644
--- a/docs/preview/03-Features/03-logging.mdx
+++ b/docs/preview/03-Features/03-logging.mdx
@@ -10,7 +10,7 @@ This page describes functionality related to logging in tests.
- The `Arcus.Testing.Logging.Xunit` library provides a `XunitTestLogger` type that's an implementation of the abstracted Microsoft [`Ilogger`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging)
+ The `Arcus.Testing.Logging.Xunit` library provides a `XunitTestLogger` type that's an implementation of the abstracted Microsoft [`ILogger`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging)
inside the [xUnit](https://xunit.net/) test framework.
**Installation**
@@ -63,7 +63,7 @@ This page describes functionality related to logging in tests.
```
- The `Arcus.Testing.Logging.NUnit` library provides a `NUnitTestLogger` type that's an implementation of the abstracted Microsoft [`Ilogger`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging)
+ The `Arcus.Testing.Logging.NUnit` library provides a `NUnitTestLogger` type that's an implementation of the abstracted Microsoft [`ILogger`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging)
inside the [NUnit](https://nunit.org/) test framework.
**Installation**
@@ -115,7 +115,7 @@ This page describes functionality related to logging in tests.
```
- The `Arcus.Testing.Logging.MSTest` library provides a `MSTestLogger` type that's an implementation of the abstracted Microsoft [`Ilogger`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging)
+ The `Arcus.Testing.Logging.MSTest` library provides a `MSTestLogger` type that's an implementation of the abstracted Microsoft [`ILogger`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging)
inside the [MSTest](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-mstest) test framework.
**Installation**
@@ -162,4 +162,38 @@ This page describes functionality related to logging in tests.
ILogger logger = factory.CreateLogger();
```
+
+ The `Arcus.Testing.Logging.TUnit` library provides a `TUnitTestLogger` type that's an implementation of the abstracted Microsoft [`ILogger`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging) inside the [TUnit](https://tunit.dev/) test framework.
+
+ **Installation**
+
+ The following functionality is available when installing this package:
+
+ ```powershell
+ PM> Install-Package -Name Arcus.Testing.Logging.TUnit
+ ```
+
+ **Example**
+
+ Log messages written to the `ILogger` instance will be written to the `TUnit`'s test logger.
+
+ ```csharp
+ using Arcus.Testing;
+ using Microsoft.Extensions.Logging;
+ using TUnit.Core.Logging;
+
+ public class TestClass
+ {
+ [Test]
+ public async Task TestMethod()
+ {
+ var tunitTestLogger = TestContext.Current.GetDefaultLogger();
+ ILogger logger = new TUnitTestLogger(tunitTestLogger);
+ }
+ }
+ ```
+
+ In the same fashion there is a:
+ * [`AddTUnitTestLogging`] extension to add a `ILoggerProvider` to a [Microsoft Logging](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-3.1) setup.
+
\ No newline at end of file
diff --git a/src/Arcus.Testing.Logging.TUnit/Arcus.Testing.Logging.TUnit.csproj b/src/Arcus.Testing.Logging.TUnit/Arcus.Testing.Logging.TUnit.csproj
new file mode 100644
index 00000000..6dbad72f
--- /dev/null
+++ b/src/Arcus.Testing.Logging.TUnit/Arcus.Testing.Logging.TUnit.csproj
@@ -0,0 +1,41 @@
+
+
+
+ net10.0
+ Arcus.Testing
+ Arcus
+ Arcus
+ Provides logging capabilities during testing within the TUnit framework
+ Copyright (c) Arcus
+ https://github.com/arcus-azure/arcus.testing
+ https://github.com/arcus-azure/arcus.testing
+ LICENSE
+ icon.png
+ README.md
+ Git
+ Azure;Testing
+ Arcus.Testing.Logging.TUnit
+ true
+ true
+ true
+ $(WarningsNotAsErrors);NU1901;NU1902;NU1903;NU1904
+ S1133
+ All
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Arcus.Testing.Logging.TUnit/Extensions/ILoggerBuilderExtensions.cs b/src/Arcus.Testing.Logging.TUnit/Extensions/ILoggerBuilderExtensions.cs
new file mode 100644
index 00000000..70c2ea8e
--- /dev/null
+++ b/src/Arcus.Testing.Logging.TUnit/Extensions/ILoggerBuilderExtensions.cs
@@ -0,0 +1,72 @@
+using System;
+using Arcus.Testing;
+using ITUnitLogger = TUnit.Core.Logging.ILogger;
+
+// ReSharper disable once CheckNamespace
+namespace Microsoft.Extensions.Logging
+{
+ ///
+ /// Extensions on the related to logging.
+ ///
+ // ReSharper disable once InconsistentNaming
+ public static class ILoggerBuilderExtensions
+ {
+ ///
+ /// Adds the logging messages from the given TUnit as a provider to the .
+ ///
+ /// The logging builder to add the NUnit logging test messages to.
+ /// The TUnit test writer to write custom test output.
+ /// Thrown when the or the is null.
+ public static ILoggingBuilder AddTUnitTestLogging(this ILoggingBuilder builder, ITUnitLogger outputWriter)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(outputWriter);
+
+#pragma warning disable CA2000 // Responsibility of disposing the created object is transferred to the caller
+ var provider = new TUnitLoggerProvider(outputWriter);
+#pragma warning restore CA2000
+ return builder.AddProvider(provider);
+ }
+
+ [ProviderAlias("TUnit")]
+ private sealed class TUnitLoggerProvider : ILoggerProvider, ISupportExternalScope
+ {
+ private readonly ITUnitLogger _testLogger;
+ private IExternalScopeProvider _scopeProvider;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal TUnitLoggerProvider(ITUnitLogger testLogger)
+ {
+ _testLogger = testLogger;
+ }
+
+ ///
+ /// Creates a new instance.
+ ///
+ /// The category name for messages produced by the logger.
+ /// The instance of that was created.
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new TUnitTestLogger(_testLogger, _scopeProvider, categoryName);
+ }
+
+ ///
+ /// Sets external scope information source for logger provider.
+ ///
+ /// The provider of scope data.
+ public void SetScopeProvider(IExternalScopeProvider scopeProvider)
+ {
+ _scopeProvider = scopeProvider;
+ }
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ public void Dispose()
+ {
+ }
+ }
+ }
+}
diff --git a/src/Arcus.Testing.Logging.TUnit/TUnitTestLogger.cs b/src/Arcus.Testing.Logging.TUnit/TUnitTestLogger.cs
new file mode 100644
index 00000000..6341ddfb
--- /dev/null
+++ b/src/Arcus.Testing.Logging.TUnit/TUnitTestLogger.cs
@@ -0,0 +1,120 @@
+using System;
+using Microsoft.Extensions.Logging;
+using ITUnitLogger = TUnit.Core.Logging.ILogger;
+using TUnitLogLevel = TUnit.Core.Logging.LogLevel;
+
+namespace Arcus.Testing
+{
+ ///
+ /// representation of a TUnit logger.
+ ///
+ public class TUnitTestLogger : ILogger
+ {
+ private readonly ITUnitLogger _outputWriter;
+ private readonly IExternalScopeProvider _scopeProvider;
+ private readonly string _categoryName;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The TUnit test writer to write custom test output.
+ /// Thrown when the is null.
+ public TUnitTestLogger(ITUnitLogger outputWriter)
+ : this(outputWriter, scopeProvider: null, categoryName: null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The TUnit test writer to write custom test output.
+ /// The instance to provide logging scopes.
+ /// The category name for messages produced by the logger.
+ /// Thrown when the is null.
+ internal TUnitTestLogger(ITUnitLogger outputWriter, IExternalScopeProvider scopeProvider, string categoryName)
+ {
+ ArgumentNullException.ThrowIfNull(outputWriter);
+ _outputWriter = outputWriter;
+ _scopeProvider = scopeProvider;
+ _categoryName = categoryName;
+ }
+
+ ///
+ /// Writes a log entry.
+ ///
+ /// Entry will be written on this level.
+ /// Id of the event.
+ /// The entry to be written. Can be also an object.
+ /// The exception related to this entry.
+ /// Function to create a message of the and .
+ /// The type of the object to be written.
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ TUnitLogLevel level = ConvertToTUnitLogLevel(logLevel);
+
+ ArgumentNullException.ThrowIfNull(formatter);
+ string message = formatter(state, exception);
+
+ var builder = new LogMessageBuilder(logLevel);
+ builder.AddCategory(_categoryName)
+ .AddUserMessage(message)
+ .AddException(exception);
+
+ _scopeProvider?.ForEachScope((st, lb) => lb.AddScope(st), builder);
+
+ string result = builder.ToString();
+ _outputWriter.Log(level, result, exception, formatter: (st, _) => st);
+ }
+
+ ///
+ /// Checks if the given is enabled.
+ ///
+ /// Level to be checked.
+ /// true if enabled.
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ TUnitLogLevel level = ConvertToTUnitLogLevel(logLevel);
+ return _outputWriter.IsEnabled(level);
+ }
+
+ private static TUnitLogLevel ConvertToTUnitLogLevel(LogLevel logLevel)
+ {
+ return logLevel switch
+ {
+ LogLevel.Critical => TUnitLogLevel.Critical,
+ LogLevel.Error => TUnitLogLevel.Error,
+ LogLevel.Warning => TUnitLogLevel.Warning,
+ LogLevel.Information => TUnitLogLevel.Information,
+ LogLevel.Debug => TUnitLogLevel.Debug,
+ LogLevel.Trace => TUnitLogLevel.Trace,
+ _ => TUnitLogLevel.None
+ };
+ }
+
+ ///
+ /// Begins a logical operation scope.
+ ///
+ /// The identifier for the scope.
+ /// The type of the state to begin scope for.
+ /// An that ends the logical operation scope on dispose.
+ public IDisposable BeginScope(TState state) where TState : notnull
+ {
+ return _scopeProvider?.Push(state);
+ }
+ }
+
+ ///
+ /// representation of a TUnit logger.
+ ///
+ /// The type whose name is used for the logger category name.
+ public class TUnitTestLogger : TUnitTestLogger, ILogger
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TUnitTestLogger(ITUnitLogger outputWriter)
+ : base(outputWriter, scopeProvider: null, categoryName: typeof(TCategoryName).FullName)
+ {
+ }
+ }
+}
diff --git a/src/Arcus.Testing.Tests.Unit/Arcus.Testing.Tests.Unit.csproj b/src/Arcus.Testing.Tests.Unit/Arcus.Testing.Tests.Unit.csproj
index e08fe121..04396103 100644
--- a/src/Arcus.Testing.Tests.Unit/Arcus.Testing.Tests.Unit.csproj
+++ b/src/Arcus.Testing.Tests.Unit/Arcus.Testing.Tests.Unit.csproj
@@ -29,6 +29,7 @@
+
diff --git a/src/Arcus.Testing.Tests.Unit/Logging/Fixture/InMemoryTUnitTestLogger.cs b/src/Arcus.Testing.Tests.Unit/Logging/Fixture/InMemoryTUnitTestLogger.cs
new file mode 100644
index 00000000..95337d78
--- /dev/null
+++ b/src/Arcus.Testing.Tests.Unit/Logging/Fixture/InMemoryTUnitTestLogger.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using TUnit.Core.Logging;
+using Xunit;
+
+namespace Arcus.Testing.Tests.Unit.Logging.Fixture
+{
+ public class MockTUnitTestLogger : ILogger
+ {
+ internal Collection<(LogLevel level, string message)> Logs { get; } = [];
+
+ public ValueTask LogAsync(LogLevel logLevel, TState state, Exception exception, Func formatter)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Log(LogLevel logLevel, TState state, Exception exception, Func formatter)
+ {
+ Logs.Add((logLevel, formatter(state, exception)));
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Verifies that there was a written for the given to this logger.
+ ///
+ public void VerifyWritten(Microsoft.Extensions.Logging.LogLevel level, string message, Exception exception = null, string state = null)
+ {
+ Assert.Contains(Logs, log =>
+ {
+ bool hasException = exception is null || log.message.Contains(exception.Message);
+ bool hasState = state is null || log.message.Contains(state);
+
+ return (int) level == (int) log.level
+ && log.message.Contains(message)
+ && hasState
+ && hasException;
+ });
+ }
+ }
+}
diff --git a/src/Arcus.Testing.Tests.Unit/Logging/Fixture/MockTestContext.cs b/src/Arcus.Testing.Tests.Unit/Logging/Fixture/MockTestContext.cs
index 00595d46..f43af84b 100644
--- a/src/Arcus.Testing.Tests.Unit/Logging/Fixture/MockTestContext.cs
+++ b/src/Arcus.Testing.Tests.Unit/Logging/Fixture/MockTestContext.cs
@@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.ObjectModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using TestContext = Microsoft.VisualStudio.TestTools.UnitTesting.TestContext;
namespace Arcus.Testing.Tests.Unit.Logging.Fixture
{
diff --git a/src/Arcus.Testing.Tests.Unit/Logging/ILoggerBuilderExtensionsTests.cs b/src/Arcus.Testing.Tests.Unit/Logging/ILoggerBuilderExtensionsTests.cs
index 66420d71..9cd853c1 100644
--- a/src/Arcus.Testing.Tests.Unit/Logging/ILoggerBuilderExtensionsTests.cs
+++ b/src/Arcus.Testing.Tests.Unit/Logging/ILoggerBuilderExtensionsTests.cs
@@ -170,5 +170,40 @@ public void AddXunitTestLogging_WithoutXunitTestLogger_Throws()
// Assert
Assert.ThrowsAny(() => builder.Build());
}
+
+ [Fact]
+ public void AddTUnitTestLogging_WithTestLogger_LogsMessage()
+ {
+ // Arrange
+ var mockLogger = new MockTUnitTestLogger();
+ var builder = new HostBuilder();
+
+ // Act
+ builder.ConfigureLogging(logging => logging.AddTUnitTestLogging(mockLogger));
+
+ // Assert
+ using IHost host = builder.Build();
+ var logger = host.Services.GetRequiredService>();
+
+ string state = Bogus.Lorem.Word();
+ using var _ = logger.BeginScope(state);
+
+ string expected = Bogus.Lorem.Sentence();
+ logger.LogInformation(expected);
+ mockLogger.VerifyWritten(LogLevel.Information, expected, state: state);
+ }
+
+ [Fact]
+ public void AddTUnitTestLogging_WithoutTestLogger_Throws()
+ {
+ // Arrange
+ var builder = new HostBuilder();
+
+ // Act
+ builder.ConfigureLogging(logging => logging.AddTUnitTestLogging(outputWriter: null));
+
+ // Assert
+ Assert.ThrowsAny(() => builder.Build());
+ }
}
}
diff --git a/src/Arcus.Testing.Tests.Unit/Logging/TUnitTestLoggerTests.cs b/src/Arcus.Testing.Tests.Unit/Logging/TUnitTestLoggerTests.cs
new file mode 100644
index 00000000..d8f6e88a
--- /dev/null
+++ b/src/Arcus.Testing.Tests.Unit/Logging/TUnitTestLoggerTests.cs
@@ -0,0 +1,30 @@
+using Arcus.Testing.Tests.Unit.Logging.Fixture;
+using Bogus;
+using Microsoft.Extensions.Logging;
+using Xunit;
+
+namespace Arcus.Testing.Tests.Unit.Logging
+{
+ public class TUnitTestLoggerTests
+ {
+ private static readonly Faker Bogus = new();
+
+ [Fact]
+ public void Log_WithLevel_SucceedsWithMicrosoftLevel()
+ {
+ // Arrange
+ string expectedMessage = Bogus.Lorem.Sentence();
+ var expectedLevel = Bogus.PickRandom();
+ var exception = Bogus.System.Exception().OrNull(Bogus);
+
+ var mockLogger = new MockTUnitTestLogger();
+ var logger = new TUnitTestLogger(mockLogger);
+
+ // Act
+ logger.Log(expectedLevel, exception, expectedMessage);
+
+ // Assert
+ mockLogger.VerifyWritten(expectedLevel, expectedMessage, exception);
+ }
+ }
+}
diff --git a/src/Arcus.Testing.sln b/src/Arcus.Testing.sln
index 3bb125e2..7012ba12 100644
--- a/src/Arcus.Testing.sln
+++ b/src/Arcus.Testing.sln
@@ -52,6 +52,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Arcus.Testing.Storage.File.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Arcus.Testing.Logging.Xunit.v3", "Arcus.Testing.Logging.Xunit.v3\Arcus.Testing.Logging.Xunit.v3.csproj", "{60883A81-B453-4E5C-B53B-B3E357E3AFB3}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Arcus.Testing.Logging.TUnit", "Arcus.Testing.Logging.TUnit\Arcus.Testing.Logging.TUnit.csproj", "{2A0C03CC-3D26-4AC4-A4B8-3B7F4DB418EF}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -120,6 +122,10 @@ Global
{60883A81-B453-4E5C-B53B-B3E357E3AFB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{60883A81-B453-4E5C-B53B-B3E357E3AFB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{60883A81-B453-4E5C-B53B-B3E357E3AFB3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2A0C03CC-3D26-4AC4-A4B8-3B7F4DB418EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2A0C03CC-3D26-4AC4-A4B8-3B7F4DB418EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2A0C03CC-3D26-4AC4-A4B8-3B7F4DB418EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2A0C03CC-3D26-4AC4-A4B8-3B7F4DB418EF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -139,6 +145,7 @@ Global
{74AB9F6E-791F-4609-96BD-15420C25AA72} = {FA2E21E0-953E-4B84-9C47-C4F0A3833E4E}
{4988575F-A1BA-4990-A7AD-F48303B51C62} = {FA2E21E0-953E-4B84-9C47-C4F0A3833E4E}
{60883A81-B453-4E5C-B53B-B3E357E3AFB3} = {45B1870D-C19A-4680-BDD8-2F670C793BC2}
+ {2A0C03CC-3D26-4AC4-A4B8-3B7F4DB418EF} = {45B1870D-C19A-4680-BDD8-2F670C793BC2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E5382820-51FF-4B00-92BE-C78E80EA0841}