[.NET Core] xUnit で log を verify する

メソッドの戻り値では検証ができないようなケースで、処理内部のログが出力されている／出力されていないによって、予期した通りの動きになっているかをテストしたかった。
Moq の Verify によって、該当ログが出力されているかどうかを確認することで実現する。

# 環境
- Visual Studio 2019
- .NET Core 3.1

# 準備
- Visual Studio で 「xUnit テストプロジェクト（.NET Core）」を作成
- 作成したテストプロジェクトに、NuGet パッケージの管理から「Moq」をインストール

# 実装
## テスト対象
``` Target.cs
    public class Hoge
    {
        private readonly ILogger<Hoge> logger;

        public Hoge(ILogger<Hoge> logger) => this.logger = logger;

        public bool Fuga()
        {
            try
            {
                // なんかの処理1
                // ～～～
                logger.LogInformation("処理1終わったよ");

                // なんかの処理2
                // ～～～
                logger.LogInformation("処理2終わったよ");

                return true;
            }
            catch
            {
                logger.LogError("例外発生しちゃたよ");
                return false;
            }
        }
    }
```
## テストコード
``` UnitTest.cs
    public class HogeTest
    {
        private Mock<ILogger<Hoge>> loggerMock = new Mock<ILogger<Hoge>>();

        [Fact]
        public void SuccessTest()
        {
            // 正常終了する条件で Fuga 実行
            new Hoge().Fuga();

            // なんかの処理1ログの確認
            loggerMock.Verify(l => l.Log(LogLevel.Information
                , It.IsAny<EventId>()
                , It.Is<It.IsAnyType>((object o, Type _) => o.ToString().Contains("処理1終わったよ"))
                , null
                , (Func<It.IsAnyType, Exception, string>)It.IsAny<object>())
            , Times.Once);  // このログが1回出力されることの確認

            // なんかの処理2ログの確認
            loggerMock.Verify(l => l.Log(LogLevel.Information
                , It.IsAny<EventId>()
                , It.Is<It.IsAnyType>((object o, Type _) => o.ToString().Contains("処理2終わったよ"))
                , null
                , (Func<It.IsAnyType, Exception, string>)It.IsAny<object>())
            , Times.Once);  // このログが1回出力されることの確認

            // 例外ログの確認
            loggerMock.Verify(l => l.Log(LogLevel.Error
                , It.IsAny<EventId>()
                , It.Is<It.IsAnyType>((object o, Type _) => o.ToString().Contains("例外発生しちゃたよ"))
                , null
                , (Func<It.IsAnyType, Exception, string>)It.IsAny<object>())
            , Times.Never);  // このログが出力されないことの確認
        }

        [Fact]
        public void ExceptionTest()
        {
            // なんかの処理2で例外が発生する条件で Fuga 実行
            new Hoge().Fuga();

            // なんかの処理1ログの確認
            loggerMock.Verify(l => l.Log(LogLevel.Information
                , It.IsAny<EventId>()
                , It.Is<It.IsAnyType>((object o, Type _) => o.ToString().Contains("処理1終わったよ"))
                , null
                , (Func<It.IsAnyType, Exception, string>)It.IsAny<object>())
            , Times.Once);  // このログが1回出力されることの確認

            // なんかの処理2ログの確認
            loggerMock.Verify(l => l.Log(LogLevel.Information
                , It.IsAny<EventId>()
                , It.Is<It.IsAnyType>((object o, Type _) => o.ToString().Contains("処理2終わったよ"))
                , null
                , (Func<It.IsAnyType, Exception, string>)It.IsAny<object>())
            , Times.Never);  // このログが出力されないことの確認

            // 例外ログの確認
            loggerMock.Verify(l => l.Log(LogLevel.Error
                , It.IsAny<EventId>()
                , It.Is<It.IsAnyType>((object o, Type _) => o.ToString().Contains("例外発生しちゃたよ"))
                , null
                , (Func<It.IsAnyType, Exception, string>)It.IsAny<object>())
            , Times.Once);  // このログが1回出力されることの確認
        }
    }
```

該当ログメッセージが出力されるか否かを Verify することで、外部から検証し辛い内部の動きを確認することができる。