这是我的控制器:

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

正如你所看到的,我有两个依赖项,一个IDAO和一个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());
    }
}

当前回答

更新(感谢@Gopal Krishnan的评论):

随着Moq >= 4.15.0,以下代码正在工作(不再需要强制转换):

 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>(),
                    It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
                Times.Once);

之前版本的答案(Moq < 4.15.0):

对于使用Moq的。net core 3答案

https://stackoverflow.com/a/54646657/2164198 https://stackoverflow.com/a/54809607/2164198 https://stackoverflow.com/a/56728528/2164198

由于在ILogger.Log中的TState曾经是对象,现在是FormattedLogValues问题中描述的变化,不再工作

幸运的是,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);

其他回答

这是一个通常放在静态helper类中的helper扩展方法:

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

当然,您可以轻松地将其扩展为模拟任何期望(即期望、消息等……)

更新。net 6与Moq 4.17.2 此扩展方法还允许使用正则表达式验证消息

static class MockHelper
{
    public static void VerifyLog<T>(this Mock<ILogger<T>> logger, LogLevel level, Times times, string? regex = null) =>
        logger.Verify(m => m.Log(
        level,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((x, y) => regex == null || Regex.IsMatch(x.ToString(), regex)),
        It.IsAny<Exception?>(),
        It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
        times);
}

这就是如何使用它

logger.VerifyLog(LogLevel.Warning, Times.Exactly(2), "Simple match");
logger.VerifyLog(LogLevel.Warning, Times.Exactly(2), "[Yy]ou\scould do regex too.*");

像其他答案建议的那样,传递模拟ILogger很容易,但是验证调用实际上是对logger进行的突然变得更加困难。原因是大多数调用实际上并不属于ILogger接口本身。

因此,大多数调用都是调用接口唯一Log方法的扩展方法。原因似乎是,如果您只有一个而不是很多重载,那么可以更容易地实现接口,从而归结为相同的方法。

当然,缺点是突然间很难验证呼叫是否已发出,因为您应该验证的呼叫与您所发出的呼叫非常不同。有一些不同的方法可以解决这个问题,我发现用于模拟框架的自定义扩展方法将使其更容易编写。

下面是我用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%,即错误消息将不会那么好,因为我们不直接检查字符串,而是检查涉及字符串的lambda,但95%总比没有好:)此外,这种方法将使测试代码

P.S.对于Moq,可以使用为Mock<ILogger<T>>编写扩展方法的方法来验证,以实现类似的结果。

P.P.S.这在。net Core 3中不再工作,查看这个线程了解更多细节:https://github.com/nsubstitute/NSubstitute/issues/597#issuecomment-573742574

更新(感谢@Gopal Krishnan的评论):

随着Moq >= 4.15.0,以下代码正在工作(不再需要强制转换):

 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>(),
                    It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
                Times.Once);

之前版本的答案(Moq < 4.15.0):

对于使用Moq的。net core 3答案

https://stackoverflow.com/a/54646657/2164198 https://stackoverflow.com/a/54809607/2164198 https://stackoverflow.com/a/56728528/2164198

由于在ILogger.Log中的TState曾经是对象,现在是FormattedLogValues问题中描述的变化,不再工作

幸运的是,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);

@Mahmoud 哈纳菲

我更新了你的答案,以配合当前的状态。

static class MockLogHelper
{
    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<It.IsAnyType>(), It.IsAny<Exception>(), (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));
        //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<It.IsAnyType>(), It.IsAny<Exception>(), (Func<It.IsAnyType, Exception, string>)It.IsAny<object>());
        //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);
    }
}

前面已经提到过,您可以将其模拟为任何其他接口。

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

到目前为止一切顺利。

好的一点是,您可以使用Moq来验证某些调用是否已经执行。例如,在这里,我检查日志已经被一个特定的异常调用。

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时,重点是针对ILooger接口中的真实Log方法,而不是扩展方法进行验证。