ASP.NET CoreでILoggerを使用してユニットテストを行う方法


128

これは私のコントローラーです:

public class BlogController : Controller
{
    private IDAO<Blog> _blogDAO;
    private readonly ILogger<BlogController> _logger;

    public BlogController(ILogger<BlogController> logger, IDAO<Blog> blogDAO)
    {
        this._blogDAO = blogDAO;
        this._logger = logger;
    }
    public IActionResult Index()
    {
        var blogs = this._blogDAO.GetMany();
        this._logger.LogInformation("Index page say hello", new object[0]);
        return View(blogs);
    }
}

ご覧のとおり、a IDAOとaの2つの依存関係があります。ILogger

これが私のテストクラスです。xUnitを使用してテストし、Moqを使用してモックとスタブを作成します。DAO簡単にモックを作成できますが、ILogger何をすべきかわからないので、nullを渡してコメントをコメントアウトし、コントローラにログインします。テストを実行するとき。テストする方法はありますが、それでもロガーを維持しますか?

public class BlogControllerTest
{
    [Fact]
    public void Index_ReturnAViewResult_WithAListOfBlog()
    {
        var mockRepo = new Mock<IDAO<Blog>>();
        mockRepo.Setup(repo => repo.GetMany(null)).Returns(GetListBlog());
        var controller = new BlogController(null,mockRepo.Object);

        var result = controller.Index();

        var viewResult = Assert.IsType<ViewResult>(result);
        var model = Assert.IsAssignableFrom<IEnumerable<Blog>>(viewResult.ViewData.Model);
        Assert.Equal(2, model.Count());
    }
}

1
ログメソッド自体が呼び出されたことを実際にテストしない場合は、Ilyaが示唆するように、モックをスタブとして使用できます。その場合、ロガーのモックは機能せず、いくつかの異なるアプローチを試すことができます。私はさまざまなアプローチを示す短い記事を書きました。この記事には、さまざまなオプションがそれぞれ含まれた完全なGitHubリポジトリが含まれています。最後に、できる限り、ILogger <T>タイプを直接操作するのではなく、独自のアダプターを使用することをお勧めします
ssmith

@ssmithが述べたように、の実際の呼び出しを確認する際にいくつかの問題がありますILogger。彼は彼のブログ投稿でいくつかの良い提案をしており、私は以下答えのほとんどのトラブルを解決するように見える私の解決策を用意しています
Ilya Chernomordik

回答:


139

それと他の依存関係をモックするだけです:

var mock = new Mock<ILogger<BlogController>>();
ILogger<BlogController> logger = mock.Object;

//or use this short equivalent 
logger = Mock.Of<ILogger<BlogController>>()

var controller = new BlogController(logger);

おそらくMicrosoft.Extensions.Logging.Abstractions使用するにはパッケージをインストールする必要がありますILogger<T>

さらに、実際のロガーを作成できます。

var serviceProvider = new ServiceCollection()
    .AddLogging()
    .BuildServiceProvider();

var factory = serviceProvider.GetService<ILoggerFactory>();

var logger = factory.CreateLogger<BlogController>();

5
デバッグ出力ウィンドウにログを記録するには、ファクトリでAddDebug()を呼び出します。var factory = serviceProvider.GetService <ILoggerFactory>()。AddDebug();
spottedmahn 2017

3
「本物のロガー」アプローチの方が効果的だと思いました。
DanielV

1
実際のロガー部分は、特定のシナリオでLogConfigurationとLogLevelをテストするのにも最適です。
Martin Lottering

このアプローチでは、スタブのみが許可され、呼び出しの検証は許可されません。私は以下答えで検証の問題のほとんどを解決するように見える私の解決策を持っています
Ilya Chernomordik

102

実際、Microsoft.Extensions.Logging.Abstractions.NullLogger<>完璧な解決策のように見えるものが見つかりました。パッケージをインストールしMicrosoft.Extensions.Logging.Abstractions、例に従って構成して使用します。

using Microsoft.Extensions.Logging;

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddSingleton<ILoggerFactory, NullLoggerFactory>();

    ...
}
using Microsoft.Extensions.Logging;

public class MyClass : IMyClass
{
    public const string ErrorMessageILoggerFactoryIsNull = "ILoggerFactory is null";

    private readonly ILogger<MyClass> logger;

    public MyClass(ILoggerFactory loggerFactory)
    {
        if (null == loggerFactory)
        {
            throw new ArgumentNullException(ErrorMessageILoggerFactoryIsNull, (Exception)null);
        }

        this.logger = loggerFactory.CreateLogger<MyClass>();
    }
}

ユニットテスト

//using Microsoft.VisualStudio.TestTools.UnitTesting;
//using Microsoft.Extensions.Logging;

[TestMethod]
public void SampleTest()
{
    ILoggerFactory doesntDoMuch = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory();
    IMyClass testItem = new MyClass(doesntDoMuch);
    Assert.IsNotNull(testItem);
}   

これは.NET Core 2.0でのみ機能し、.NET Core 1.1では機能しないようです。
ThorkilVærge18年1

3
@adospace、あなたのコメントは答えよりもはるかに有用です
ジョニー5

これがどのように機能するかの例を挙げられますか?単体テストのときに、出力ウィンドウにログを表示したいのですが、これがそうであるかどうかはわかりません。
J86

@adospaceこれはstartup.csに入るはずですか?
raklos

1
@raklosハム、ServiceCollectionがインスタンス化されるテスト内の起動メソッドで使用することは想定されていません
adospace

31

ITestOutputHelper(xunitから)を使用するカスタムロガーを使用して、出力とログをキャプチャします。以下は、のみを書き込む小さなサンプルですstate、出力に。

public class XunitLogger<T> : ILogger<T>, IDisposable
{
    private ITestOutputHelper _output;

    public XunitLogger(ITestOutputHelper output)
    {
        _output = output;
    }
    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        _output.WriteLine(state.ToString());
    }

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

    public IDisposable BeginScope<TState>(TState state)
    {
        return this;
    }

    public void Dispose()
    {
    }
}

のようなユニットテストで使用してください

public class BlogControllerTest
{
  private XunitLogger<BlogController> _logger;

  public BlogControllerTest(ITestOutputHelper output){
    _logger = new XunitLogger<BlogController>(output);
  }

  [Fact]
  public void Index_ReturnAViewResult_WithAListOfBlog()
  {
    var mockRepo = new Mock<IDAO<Blog>>();
    mockRepo.Setup(repo => repo.GetMany(null)).Returns(GetListBlog());
    var controller = new BlogController(_logger,mockRepo.Object);
    // rest
  }
}

1
こんにちは。これは私にとってはうまくいきます。ログ情報を確認または表示する方法
malik saifullah

VSから直接ユニットテストケースを実行しています。そのためのコンソールはありません
malik saifullah

1
@maliksaifullah imリシャーパーを使用しています。それをvsで確認しましょう
Jehof

1
@maliksaifullah VSのTestExplorerは、テストの出力を開くためのリンクを提供します。TestExplorerでテストを選択すると、下部にリンクがあります
Jehof

1
ありがとうございます。いくつかの提案:1)typeパラメータが使用されないため、これはジェネリックである必要はありません。just ILoggerを実装すると、より広く使用できるようになります。2)BeginScopeはそれ自体を返すべきではありません。実行中にスコープを開始および終了するテスト済みのメソッドがロガーを破棄するためです。代わりに、そのIDisposableインスタンスを実装して返すプライベートな「ダミー」のネストされたクラスを作成します(次にIDisposableから削除しますXunitLogger)。
トビアスJ

27

Moqを使用している.netコア3の回答の場合

幸いなことにstakxは素晴らしい回避策を提供してくれました。だから私はそれが他の人のために時間を節約できることを願って投稿しています(物事を理解するのにしばらく時間がかかりました):

 loggerMock.Verify(
                x => x.Log(
                    LogLevel.Information,
                    It.IsAny<EventId>(),
                    It.Is<It.IsAnyType>((o, t) => string.Equals("Index page say hello", o.ToString(), StringComparison.InvariantCultureIgnoreCase)),
                    It.IsAny<Exception>(),
                    (Func<It.IsAnyType, Exception, string>) It.IsAny<object>()),
                Times.Once);

あなたは私の日を救った。ありがとう。
KiddoDeveloper

15

私の2セントを追加すると、これは通常、静的ヘルパークラスに配置されるヘルパー拡張メソッドです。

static class MockHelper
{
    public static ISetup<ILogger<T>> MockLog<T>(this Mock<ILogger<T>> logger, LogLevel level)
    {
        return logger.Setup(x => x.Log(level, It.IsAny<EventId>(), It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>()));
    }

    private static Expression<Action<ILogger<T>>> Verify<T>(LogLevel level)
    {
        return x => x.Log(level, 0, It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>());
    }

    public static void Verify<T>(this Mock<ILogger<T>> mock, LogLevel level, Times times)
    {
        mock.Verify(Verify<T>(level), times);
    }
}

次に、次のように使用します。

//Arrange
var logger = new Mock<ILogger<YourClass>>();
logger.MockLog(LogLevel.Warning)

//Act

//Assert
logger.Verify(LogLevel.Warning, Times.Once());

そしてもちろん、簡単に拡張して任意の期待(つまり、期待、メッセージなど)を模擬することができます。


これは非常にエレガントなソリューションです。
MichaelDotKnox

私は同意します、この答えはとても良かったです。投票数が
それほど

1
ファブ これは非ジェネリックのバージョンですILoggergist.github.com/timabell/d71ae82c6f3eaa5df26b147f9d3842eb
Tim Abell

LogWarningで渡した文字列をチェックするモックを作成することは可能でしょうか?例:It.Is<string>(s => s.Equals("A parameter is empty!"))
セルハット

これは非常に役立ちます。私にとって欠けているのは、XUnit出力に書き込むモックにコールバックを設定する方法です。コールバックを打つことはありません。
フリップダウト

6

他の回答がmockを渡すように提案しているので簡単ILoggerですが、実際にロガーへの呼び出しが行われたことを確認することは突然はるかに問題になります。その理由は、ほとんどの呼び出しが実際にはILoggerインターフェース自体に属していないためです。

したがって、ほとんどの呼び出しは、 Log、インターフェイスの。理由は、同じメソッドにまとめるオーバーロードが1つだけで、多くない場合は、インターフェイスを実装する方が簡単だからです。

もちろん、欠点は、確認する必要のある呼び出しが、行った呼び出しと非常に異なるため、呼び出しが行われたことを確認するのが突然はるかに困難になることです。これを回避するにはいくつかの異なるアプローチがあり、フレームワークをモックするためのカスタム拡張メソッドが最も簡単に記述できることがわかりました。

これは私が使用するために作成したメソッドの例ですNSubstitute

public static class LoggerTestingExtensions
{
    public static void LogError(this ILogger logger, string message)
    {
        logger.Log(
            LogLevel.Error,
            0,
            Arg.Is<FormattedLogValues>(v => v.ToString() == message),
            Arg.Any<Exception>(),
            Arg.Any<Func<object, Exception, string>>());
    }

}

そしてこれはそれがどのように使用できるかです:

_logger.Received(1).LogError("Something bad happened");   

メソッドを直接使用した場合とまったく同じように見えますが、ここで重要なのは、拡張メソッドが名前空間で元のメソッドよりも「近い」ため、優先されるため、代わりに使用されることです。

残念ながら、私たちが望むものを100%提供するわけではありません。つまり、文字列を直接チェックするのではなく、文字列を含むラムダをチェックするため、エラーメッセージはそれほど良くありませんが、95%は何もしないよりはましです:)さらにこのアプローチはテストコードを作成します

PS Moqの場合、拡張メソッドを作成するアプローチを使用Mock<ILogger<T>>Verifyて、同様の結果を得ることができます。

PPSこれは.Net Core 3では機能しなくなりました。詳細については、次のスレッドを確認してください。https//github.com/nsubstitute/NSubstitute/issues/597#issuecomment-573742574


ロガー呼び出しを確認するのはなぜですか?これらはビジネスロジックの一部ではありません。何か悪いことが起こった場合は、メッセージをログに記録するのではなく、実際のプログラムの動作(エラーハンドラーの呼び出しや例外のスローなど)を確認したいと思います。
Ilya Chumakov

1
まあ、少なくとも場合によっては、それをテストすることも非常に重要だと思います。プログラムがサイレントに失敗することが何度もありました。そのため、例外が発生したときにロギングが行われたことを確認することは理にかなっています。たとえば、「どちらか」ではなく、実際のプログラムの動作とロギングの両方をテストします。
Ilya Chernomordik

5

他のインターフェースと同じようにモックできることはすでに述べました。

var logger = new Mock<ILogger<QueuedHostedService>>();

ここまでは順調ですね。

良い点は、特定の呼び出しが実行されたことMoq確認するために使用できることです。たとえば、ここでは特定のでログが呼び出されていることを確認しますException

logger.Verify(m => m.Log(It.Is<LogLevel>(l => l == LogLevel.Information), 0,
            It.IsAny<object>(), It.IsAny<TaskCanceledException>(), It.IsAny<Func<object, Exception, string>>()));

Verifyポイントを使用するときは、拡張メソッドではなく、インターフェースLogからの実際のメソッドに対してそれを行うことILoogerです。


5

@ ivan-samyginと@stakxの作業をさらに発展させて、例外とすべてのログ値(KeyValuePairs)でも一致できる拡張メソッドを次に示します。

これらは.Net Core 3、Moq 4.13.0およびMicrosoft.Extensions.Logging.Abstractions 3.1.0で(私のマシン上で)動作します。

/// <summary>
/// Verifies that a Log call has been made, with the given LogLevel, Message and optional KeyValuePairs.
/// </summary>
/// <typeparam name="T">Type of the class for the logger.</typeparam>
/// <param name="loggerMock">The mocked logger class.</param>
/// <param name="expectedLogLevel">The LogLevel to verify.</param>
/// <param name="expectedMessage">The Message to verify.</param>
/// <param name="expectedValues">Zero or more KeyValuePairs to verify.</param>
public static void VerifyLog<T>(this Mock<ILogger<T>> loggerMock, LogLevel expectedLogLevel, string expectedMessage, params KeyValuePair<string, object>[] expectedValues)
{
    loggerMock.Verify(mock => mock.Log(
        expectedLogLevel,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((o, t) => MatchesLogValues(o, expectedMessage, expectedValues)),
        It.IsAny<Exception>(),
        It.IsAny<Func<object, Exception, string>>()
        )
    );
}

/// <summary>
/// Verifies that a Log call has been made, with LogLevel.Error, Message, given Exception and optional KeyValuePairs.
/// </summary>
/// <typeparam name="T">Type of the class for the logger.</typeparam>
/// <param name="loggerMock">The mocked logger class.</param>
/// <param name="expectedMessage">The Message to verify.</param>
/// <param name="expectedException">The Exception to verify.</param>
/// <param name="expectedValues">Zero or more KeyValuePairs to verify.</param>
public static void VerifyLog<T>(this Mock<ILogger<T>> loggerMock, string expectedMessage, Exception expectedException, params KeyValuePair<string, object>[] expectedValues)
{
    loggerMock.Verify(logger => logger.Log(
        LogLevel.Error,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((o, t) => MatchesLogValues(o, expectedMessage, expectedValues)),
        It.Is<Exception>(e => e == expectedException),
        It.Is<Func<It.IsAnyType, Exception, string>>((o, t) => true)
    ));
}

private static bool MatchesLogValues(object state, string expectedMessage, params KeyValuePair<string, object>[] expectedValues)
{
    const string messageKeyName = "{OriginalFormat}";

    var loggedValues = (IReadOnlyList<KeyValuePair<string, object>>)state;

    return loggedValues.Any(loggedValue => loggedValue.Key == messageKeyName && loggedValue.Value.ToString() == expectedMessage) &&
           expectedValues.All(expectedValue => loggedValues.Any(loggedValue => loggedValue.Key == expectedValue.Key && loggedValue.Value == expectedValue.Value));
}


1

ダミーを作成するだけでILoggerは、単体テストにはあまり価値がありません。また、ロギング呼び出しが行われたことを確認する必要があります。Moqを使用してモックILogger挿入できますが、呼び出しの確認は少し難しい場合があります。この記事では、Moqによる検証について詳しく説明します。

これは、記事の非常に単純な例です。

_loggerMock.Verify(l => l.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()), Times.Exactly(1));

情報メッセージが記録されたことを確認します。ただし、メッセージテンプレートや名前付きプロパティなど、メッセージに関するより複雑な情報を確認したい場合は、さらに複雑になります。

_loggerMock.Verify
(
    l => l.Log
    (
        //Check the severity level
        LogLevel.Error,
        //This may or may not be relevant to your scenario
        It.IsAny<EventId>(),
        //This is the magical Moq code that exposes internal log processing from the extension methods
        It.Is<It.IsAnyType>((state, t) =>
            //This confirms that the correct log message was sent to the logger. {OriginalFormat} should match the value passed to the logger
            //Note: messages should be retrieved from a service that will probably store the strings in a resource file
            CheckValue(state, LogTest.ErrorMessage, "{OriginalFormat}") &&
            //This confirms that an argument with a key of "recordId" was sent with the correct value
            //In Application Insights, this will turn up in Custom Dimensions
            CheckValue(state, recordId, nameof(recordId))
    ),
    //Confirm the exception type
    It.IsAny<NotImplementedException>(),
    //Accept any valid Func here. The Func is specified by the extension methods
    (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()),
    //Make sure the message was logged the correct number of times
    Times.Exactly(1)
);

他のモックフレームワークでも同じことができると思いますが、ILoggerインターフェースはそれを難し​​くしています。


1
私はその感情に同意します、そしてあなたが言うように、それは表現を構築するのが少し難しいかもしれません。私はしばしば同じ問題を抱えていたので、最近、Moq.Contrib.ExpressionBuilders.Loggingを組み合わせて、より口当たりの良い滑らかなインターフェースを提供しました。
rgvlee

1

まだ実際の場合。.netコア> = 3のテストで出力にログを記録する簡単な方法

[Fact]
public void SomeTest()
{
    using var logFactory = LoggerFactory.Create(builder => builder.AddConsole());
    var logger = logFactory.CreateLogger<AccountController>();
    
    var controller = new SomeController(logger);

    var result = controller.SomeActionAsync(new Dto{ ... }).GetAwaiter().GetResult();
}


0

私はNSubstituteを使用してそのLoggerインターフェースを模擬しようとしました(そしてArg.Any<T>()、提供できない型パラメーターを要求するため失敗しました)が、次の方法でテストロガー(@jehofの回答と同様)を作成することになりました。

    internal sealed class TestLogger<T> : ILogger<T>, IDisposable
    {
        private readonly List<LoggedMessage> _messages = new List<LoggedMessage>();

        public IReadOnlyList<LoggedMessage> Messages => _messages;

        public void Dispose()
        {
        }

        public IDisposable BeginScope<TState>(TState state)
        {
            return this;
        }

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

        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
        {
            var message = formatter(state, exception);
            _messages.Add(new LoggedMessage(logLevel, eventId, exception, message));
        }

        public sealed class LoggedMessage
        {
            public LogLevel LogLevel { get; }
            public EventId EventId { get; }
            public Exception Exception { get; }
            public string Message { get; }

            public LoggedMessage(LogLevel logLevel, EventId eventId, Exception exception, string message)
            {
                LogLevel = logLevel;
                EventId = eventId;
                Exception = exception;
                Message = message;
            }
        }
    }

ログに記録されたすべてのメッセージに簡単にアクセスし、提供されているすべての意味のあるパラメーターをアサートできます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.