You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用NUnit和Moq测试ILogger<>时遇到NotSupportedException异常

使用NUnit和Moq测试ILogger<>时遇到NotSupportedException异常

嘿,我一眼就看出你遇到了Moq测试ILogger时的经典坑!错误信息里已经说得明明白白:像LogInformation这类日志扩展方法,没法在Moq的Setup或Verify表达式里直接使用——因为Moq只能拦截接口的实例方法,而LogInformation是LoggerExtensions里的静态扩展方法,根本不是ILogger接口的成员,自然没法被Moq拦截验证。

别慌,我们换个思路,直接验证ILogger接口定义的底层Log实例方法就行,所有日志扩展方法最终都会调用这个方法。

解决方法:验证ILogger的Log方法

把原来针对LogInformation的Verify代码,替换成对Log方法的验证,同时匹配对应的日志级别、消息模板和参数。

修改你测试里的Assert部分,替换掉原来的loggerMock.Verify代码:

// 验证"Begin Request"日志
loggerMock.Verify(
    x => x.Log(
        LogLevel.Information, // 匹配日志级别
        It.IsAny<EventId>(), // 事件ID一般为0,用IsAny即可
        It.Is<It.IsAnyType>((state, type) =>
            // 检查日志消息模板是否匹配
            state.ToString().Contains("Begin Request: Id: {UniqueId}, request name: {RequestName}") &&
            // 检查日志参数是否包含预期的键
            ((IReadOnlyList<KeyValuePair<string, object>>)state).Any(kvp => kvp.Key == "UniqueId") &&
            ((IReadOnlyList<KeyValuePair<string, object>>)state).Any(kvp => kvp.Key == "RequestName")
        ),
        It.IsAny<Exception>(), // 此处无异常,用IsAny
        It.IsAny<Func<It.IsAnyType, Exception, string>>() // 格式化委托,用IsAny
    ),
    Times.Once
);

// 验证"Audity Request"日志
loggerMock.Verify(
    x => x.Log(
        LogLevel.Information,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((state, type) =>
            state.ToString().Contains("Audity Request: {Name} {@Request} , total request time:{ElapsedMilliseconds}") &&
            ((IReadOnlyList<KeyValuePair<string, object>>)state).Any(kvp => kvp.Key == "Name" && (string)kvp.Value == typeof(TestRequest).Name) &&
            ((IReadOnlyList<KeyValuePair<string, object>>)state).Any(kvp => kvp.Key == "Request" && kvp.Value == request) &&
            ((IReadOnlyList<KeyValuePair<string, object>>)state).Any(kvp => kvp.Key == "ElapsedMilliseconds")
        ),
        It.IsAny<Exception>(),
        It.IsAny<Func<It.IsAnyType, Exception, string>>()
    ),
    Times.Once
);

代码解释

  • Log是ILogger接口的核心实例方法,所有日志扩展方法都会调用它,所以Moq可以正常拦截验证。
  • It.IsAnyType用来匹配日志内部的FormattedLogValues类型(这个是内部类型,没法直接引用,所以用这个通配符)。
  • 通过state.ToString()可以获取日志的消息模板,用来验证是否符合预期内容。
  • 把state转换成IReadOnlyList<KeyValuePair<string, object>>,就能访问日志里的所有参数键值对,验证参数是否符合预期。

完整修改后的测试类

using Application.Common.Behaviours;
using MediatR;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;

namespace Application.UnitTests.Common.Behaviours;

public class RequestLoggerTests {

    [Test]
    public async Task ShouldVerifyThatLoggerHasBeenCalledOnceIfInvoked() {
        // Arrange
        var loggerMock = new Mock<ILogger<TestRequest>>();
        var request = new TestRequest();
        var response = new TestResponse();
        var nextDelegateMock = new Mock<RequestHandlerDelegate<TestResponse>>();
        var cancellationToken = CancellationToken.None;

        nextDelegateMock.Setup(x => x()).ReturnsAsync(response);

        var behaviour = new LoggingBehaviour<TestRequest, TestResponse>(loggerMock.Object);

        // Act
        var result = await behaviour.Handle(request, nextDelegateMock.Object, cancellationToken);

        // Assert
        nextDelegateMock.Verify(x => x(), Times.Once);

        // 验证Begin Request日志
        loggerMock.Verify(
            x => x.Log(
                LogLevel.Information,
                It.IsAny<EventId>(),
                It.Is<It.IsAnyType>((state, type) =>
                    state.ToString().Contains("Begin Request: Id: {UniqueId}, request name: {RequestName}") &&
                    ((IReadOnlyList<KeyValuePair<string, object>>)state).Any(kvp => kvp.Key == "UniqueId") &&
                    ((IReadOnlyList<KeyValuePair<string, object>>)state).Any(kvp => kvp.Key == "RequestName")
                ),
                It.IsAny<Exception>(),
                It.IsAny<Func<It.IsAnyType, Exception, string>>()
            ),
            Times.Once
        );

        // 验证Audity Request日志
        loggerMock.Verify(
            x => x.Log(
                LogLevel.Information,
                It.IsAny<EventId>(),
                It.Is<It.IsAnyType>((state, type) =>
                    state.ToString().Contains("Audity Request: {Name} {@Request} , total request time:{ElapsedMilliseconds}") &&
                    ((IReadOnlyList<KeyValuePair<string, object>>)state).Any(kvp => kvp.Key == "Name" && (string)kvp.Value == typeof(TestRequest).Name) &&
                    ((IReadOnlyList<KeyValuePair<string, object>>)state).Any(kvp => kvp.Key == "Request" && kvp.Value == request) &&
                    ((IReadOnlyList<KeyValuePair<string, object>>)state).Any(kvp => kvp.Key == "ElapsedMilliseconds")
                ),
                It.IsAny<Exception>(),
                It.IsAny<Func<It.IsAnyType, Exception, string>>()
            ),
            Times.Once
        );

        Assert.AreEqual(response, result);
    }
}

public class TestResponse { }
public class TestRequest { }

这样修改后,你的测试应该就能正常通过,不会再抛出NotSupportedException了!

备注:内容来源于stack exchange,提问作者hackp0int

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.22 07:13:11