xUnit の単体テストで ILogger への出力を実現する

# やりたいこと

- xUnit による単体テストの中で `ILogger<T>` が注入されるクラスをテストする
- 記録される内容を確認できるようにする

# 環境
- Microsoft Visual Studio Community 2022 Version 17.9.7
- .NET8 C# 12

# テスト対象

```cs:MessageService.cs
using Microsoft.Extensions.Logging;

namespace HigeDaruma.DemoXUnit.ClassLibrary.Messages;

public sealed class MessageService(ILogger<MessageService> logger)
{
    public string GetMessage(string name)
    {
        logger.LogInformation("Method:{Method},Name:{Name}", "MessageService.GetMessage(string)", name);

        return $"Hello, World {name} !";
    }
}
```

プライマリーコンストラクターに注入された `ILogger<T>` を使ってロギングしています。このクラスをテストできるようにし、`LogInformation` の内容を確認できるようにします。

# 手順

## まずは ITestOutputHelper をつかってみる

xUnit で単体テストを実装した場合、テストエクスプローラーにログを表示するには `ITestOutputHelper` を活用します。

```cs:MessageServiceTest.cs
using Xunit.Abstractions;

namespace HigeDaruma.DemoXUnit.ClassLibrary.UnitTest.Messages;

public sealed class MessageServiceTest
{
    private readonly ITestOutputHelper _testOutputHelper;

    public MessageServiceTest(ITestOutputHelper testOutputHelper)
    {
        _testOutputHelper = testOutputHelper;
    }

    [Fact]
    public void TestOutputHelperTest()
    {
        string name = "HigeDaruma";

        _testOutputHelper.WriteLine(name);
    }
}

```

個のテストを実行すると、テストエクスプローラーの「テスト詳細の概要」（どっちやねん）の「標準出力」の領域に `WriteLine()` した内容が表示されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1107938/76aa09c5-7f51-e2c8-0966-f179e237daf2.png)

この `ITestOutputHelper` を `ILogger<T>` として注入できるようにすれば、やりたいことを実現できそうです。

## xUnit プロジェクトに NuGet パッケージを追加する

1. `Microsoft.Extensions.Logging` を追加します

## Logger/LoggerProvider を実装する

1. `ILogger` を実装する `XunitLogger` を追加します

```cs:XunitLogger.cs
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;

namespace HigeDaruma.DemoXUnit.ClassLibrary.UnitTest;

internal sealed class XunitLogger(
    ITestOutputHelper testOutputHelper,
    string categoryName) : ILogger
{
    public IDisposable? BeginScope<TState>(TState state) where TState : notnull
    {
        return NopDisposable.Instance;
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        return true;
    }

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
    {
        testOutputHelper.WriteLine(($"{categoryName} [{eventId}] {formatter(state, exception)}");

        if (exception is not null)
        {
            testOutputHelper.WriteLine(exception.ToString());
        }
    }

    private class NopDisposable : IDisposable
    {
        public static NopDisposable Instance = new();
        public void Dispose()
        { }
    }
}
```

2\. `ILoggerProvider` を実装する `XunitLoggerProvider` を追加する

```cs:XunitLoggerProvider.cs
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;

namespace HigeDaruma.DemoXUnit.ClassLibrary.UnitTest;

internal sealed class XunitLoggerProvider(
    ITestOutputHelper testOutputHelper) : ILoggerProvider
{
    public ILogger CreateLogger(string categoryName)
    {
        return new XunitLogger(testOutputHelper, categoryName);
    }

    public void Dispose()
    {
    }
}
```

3\. `XunitLogger` を `ILogger<T>` として注入できるように準備

```diff_c_sharp:MessageServiceTest.cs
using Xunit.Abstractions;

namespace HigeDaruma.DemoXUnit.ClassLibrary.UnitTest.Messages;

public sealed class MessageServiceTest
{
    private readonly ITestOutputHelper _testOutputHelper;
+
+    private readonly ILogger<MessageService> _logger;

    public MessageServiceTest(ITestOutputHelper testOutputHelper)
    {
        _testOutputHelper = testOutputHelper;
+
+        LoggerFactory loggerFactory = new();
+        loggerFactory.AddProvider(new XunitLoggerProvider(testOutputHelper));
+        _logger = loggerFactory.CreateLogger<MessageService>();
    }

    [Fact]
    public void TestOutputHelperTest()
    {
        string name = "HigeDaruma";

        _testOutputHelper.WriteLine(name);
    }
}
```

4\. テストメソッドで使用する

```cs:MessageSAerviceTest.cs
    [Fact]
    public void GetMessage_ReturnsOk_WhenInputNameIsOk()
    {
        // Arrange
        string name = "HigeDaruma";
        MessageService instance = new(_logger);

        // Act
        string result = instance.GetMessage(name);

        // Assert
        Assert.Equal("Hello, World HigeDaruma !", result);
    }
```

`MessageService` のコンストラクターに `_logger` を注入し、`GetMessage(string)` を実行します。インスタンスを生成できるだけではなく、テストエクスプローラーにもログ内容が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1107938/b6aba7cb-637a-b92b-b5f8-cefbb5ba3a10.png)

# まとめ

xUnit でデバッグに使える `ITestOutputHelper` を使って `ILogger<T>` を実現しました。これで単体テストがはかどります。

# 参考

https://kuttsun.blogspot.com/2019/11/ilogger-itestoutputhelper.html