使用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
相关产品推荐
相关产品推荐

