web-dev-qa-db-ja.com

ASP.NET CoreでILoggerを使用して単体テストを行う方法

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

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);
    }
}

ご覧のとおり、2つの依存関係、IDAOILoggerがあります

これは私のテストクラスです。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());
    }
}
61
duc

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

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>();
69
Ilya Chumakov

実際、完璧なソリューションのように見えるMicrosoft.Extensions.Logging.Abstractions.NullLogger<>を見つけました。

56
Amir Shitrit

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
  }
}
10
Jehof

他の答えがモック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");   

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

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

追伸Moqでは、Verifyを実行するMock<ILogger<T>>の拡張メソッドを記述するアプローチを使用して、同様の結果を達成できます。

1

既に他のインターフェイスとしてモックできると述べました。

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メソッドに対して実行することです。

1
guillem

そして、StructureMap/Lamarを使用する場合:

var c = new Container(_ =>
{
    _.For(typeof(ILogger<>)).Use(typeof(NullLogger<>));
});

ドキュメント:

1