Skip to content
Open
40 changes: 37 additions & 3 deletions docs/preview/03-Features/03-logging.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This page describes functionality related to logging in tests.

<Tabs groupId="testing-frameworks">
<TabItem value="xunit" label="xUnit" default>
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**
Expand Down Expand Up @@ -63,7 +63,7 @@ This page describes functionality related to logging in tests.
```
</TabItem>
<TabItem value="nunit" label="NUnit">
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**
Expand Down Expand Up @@ -115,7 +115,7 @@ This page describes functionality related to logging in tests.
```
</TabItem>
<TabItem value="mstest" label="MSTest">
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**
Expand Down Expand Up @@ -162,4 +162,38 @@ This page describes functionality related to logging in tests.
ILogger logger = factory.CreateLogger<TestClass>();
```
</TabItem>
<TabItem value="tunit" label="TUnit">
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.
</TabItem>
</Tabs>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Arcus.Testing</RootNamespace>
<Authors>Arcus</Authors>
<Company>Arcus</Company>
<Description>Provides logging capabilities during testing within the TUnit framework</Description>
<Copyright>Copyright (c) Arcus</Copyright>
<PackageProjectUrl>https://github.com/arcus-azure/arcus.testing</PackageProjectUrl>
<RepositoryUrl>https://github.com/arcus-azure/arcus.testing</RepositoryUrl>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryType>Git</RepositoryType>
<PackageTags>Azure;Testing</PackageTags>
<PackageId>Arcus.Testing.Logging.TUnit</PackageId>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsNotAsErrors>$(WarningsNotAsErrors);NU1901;NU1902;NU1903;NU1904</WarningsNotAsErrors>
<NoWarn>S1133</NoWarn>
<AnalysisMode>All</AnalysisMode>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\Arcus.Testing.Logging.Xunit.v3\LogMessageBuilder.cs" Link="LogMessageBuilder.cs" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
<None Include="..\..\LICENSE" Pack="true" PackagePath="\" />
<None Include="..\..\docs\static\img\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="TUnit.Core" Version="[1.*,2.0.0)" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="[10.*,11.0.0)" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System;
using Arcus.Testing;
using ITUnitLogger = TUnit.Core.Logging.ILogger;

// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.Logging
{
/// <summary>
/// Extensions on the <see cref="ILoggingBuilder"/> related to logging.
/// </summary>
// ReSharper disable once InconsistentNaming
public static class ILoggerBuilderExtensions
{
/// <summary>
/// Adds the logging messages from the given TUnit <paramref name="outputWriter"/> as a provider to the <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The logging builder to add the NUnit logging test messages to.</param>
/// <param name="outputWriter">The TUnit test writer to write custom test output.</param>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="builder"/> or the <paramref name="outputWriter"/> is <c>null</c>.</exception>
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;

/// <summary>
/// Initializes a new instance of the <see cref="TUnitLoggerProvider"/> class.
/// </summary>
internal TUnitLoggerProvider(ITUnitLogger testLogger)
{
_testLogger = testLogger;
}

/// <summary>
/// Creates a new <see cref="ILogger" /> instance.
/// </summary>
/// <param name="categoryName">The category name for messages produced by the logger.</param>
/// <returns>The instance of <see cref="ILogger" /> that was created.</returns>
public ILogger CreateLogger(string categoryName)
{
return new TUnitTestLogger(_testLogger, _scopeProvider, categoryName);
}

/// <summary>
/// Sets external scope information source for logger provider.
/// </summary>
/// <param name="scopeProvider">The provider of scope data.</param>
public void SetScopeProvider(IExternalScopeProvider scopeProvider)
{
_scopeProvider = scopeProvider;
}

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
}
}
}
}
120 changes: 120 additions & 0 deletions src/Arcus.Testing.Logging.TUnit/TUnitTestLogger.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// <see cref="ILogger"/> representation of a TUnit logger.
/// </summary>
public class TUnitTestLogger : ILogger
{
private readonly ITUnitLogger _outputWriter;
private readonly IExternalScopeProvider _scopeProvider;
private readonly string _categoryName;

/// <summary>
/// Initializes a new instance of the <see cref="TUnitTestLogger"/> class.
/// </summary>
/// <param name="outputWriter">The TUnit test writer to write custom test output.</param>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="outputWriter"/> is <c>null</c>.</exception>
public TUnitTestLogger(ITUnitLogger outputWriter)
: this(outputWriter, scopeProvider: null, categoryName: null)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="TUnitTestLogger"/> class.
/// </summary>
/// <param name="outputWriter">The TUnit test writer to write custom test output.</param>
/// <param name="scopeProvider">The instance to provide logging scopes.</param>
/// <param name="categoryName">The category name for messages produced by the logger.</param>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="outputWriter"/> is <c>null</c>.</exception>
internal TUnitTestLogger(ITUnitLogger outputWriter, IExternalScopeProvider scopeProvider, string categoryName)
{
ArgumentNullException.ThrowIfNull(outputWriter);
_outputWriter = outputWriter;
_scopeProvider = scopeProvider;
_categoryName = categoryName;
}

/// <summary>
/// Writes a log entry.
/// </summary>
/// <param name="logLevel">Entry will be written on this level.</param>
/// <param name="eventId">Id of the event.</param>
/// <param name="state">The entry to be written. Can be also an object.</param>
/// <param name="exception">The exception related to this entry.</param>
/// <param name="formatter">Function to create a <see cref="String" /> message of the <paramref name="state" /> and <paramref name="exception" />.</param>
/// <typeparam name="TState">The type of the object to be written.</typeparam>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> 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);
}

/// <summary>
/// Checks if the given <paramref name="logLevel" /> is enabled.
/// </summary>
/// <param name="logLevel">Level to be checked.</param>
/// <returns><c>true</c> if enabled.</returns>
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
};
}

/// <summary>
/// Begins a logical operation scope.
/// </summary>
/// <param name="state">The identifier for the scope.</param>
/// <typeparam name="TState">The type of the state to begin scope for.</typeparam>
/// <returns>An <see cref="IDisposable" /> that ends the logical operation scope on dispose.</returns>
public IDisposable BeginScope<TState>(TState state) where TState : notnull
{
return _scopeProvider?.Push(state);
}
}

/// <summary>
/// <see cref="ILogger"/> representation of a TUnit logger.
/// </summary>
/// <typeparam name="TCategoryName">The type whose name is used for the logger category name.</typeparam>
public class TUnitTestLogger<TCategoryName> : TUnitTestLogger, ILogger<TCategoryName>
{
/// <summary>
/// Initializes a new instance of the <see cref="TUnitTestLogger"/> class.
/// </summary>
public TUnitTestLogger(ITUnitLogger outputWriter)
: base(outputWriter, scopeProvider: null, categoryName: typeof(TCategoryName).FullName)
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<ProjectReference Include="..\Arcus.Testing.Core\Arcus.Testing.Core.csproj" />
<ProjectReference Include="..\Arcus.Testing.Assert\Arcus.Testing.Assert.csproj" />
<ProjectReference Include="..\Arcus.Testing.Integration.DataFactory\Arcus.Testing.Integration.DataFactory.csproj" />
<ProjectReference Include="..\Arcus.Testing.Logging.TUnit\Arcus.Testing.Logging.TUnit.csproj" />
<ProjectReference Include="..\Arcus.Testing.Logging.Xunit.v3\Arcus.Testing.Logging.Xunit.v3.csproj" Aliases="ArcusXunitV3" />
<ProjectReference Include="..\Arcus.Testing.Logging.Xunit\Arcus.Testing.Logging.Xunit.csproj" Aliases="ArcusXunitV2" />
<ProjectReference Include="..\Arcus.Testing.Logging.NUnit\Arcus.Testing.Logging.NUnit.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TState>(LogLevel logLevel, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
throw new NotImplementedException();
}

public void Log<TState>(LogLevel logLevel, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
Logs.Add((logLevel, formatter(state, exception)));
}

public bool IsEnabled(LogLevel logLevel)
{
throw new NotImplementedException();
}

/// <summary>
/// Verifies that there was a <paramref name="message"/> written for the given <paramref name="level"/> to this logger.
/// </summary>
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;
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Loading
Loading