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

.NET Core中NUnit测试ILogger依赖注入共享状态问题排查

问题分析与解决方案

问题场景

我有一个通过依赖注入(DI)注入ILogger<Microsoft.Extensions.Logging>的IHostedService,该服务用Timer类每60秒执行一次,需要验证日志记录器被调用一次。原本单个测试类里用[OneTimeSetup]创建ILogger<Type>的模拟对象时测试正常,但新增第二个同样注入ILogger的测试类(同一测试项目,测试不同类)后,第一个测试开始失败。

仅在以下场景测试会失败:

  • 调试模式下运行
  • 与另一个测试类一起运行

已排查共享状态:两个测试类都用[OneTimeSetup]创建独立的模拟对象,不存在共享实例的情况。

移除失败的TestOne后,TestOne里的另一个测试会无限运行,必须手动关闭VS才能终止。

同时运行测试时,TestOne抛出异常:

Expected invocation on the mock once, but was 0 times:
logger => logger.Log<It.IsAnyType>(It.Is(logLevel => (int)logLevel == 2),
It.IsAny(),
It.IsAny<It.IsAnyType>(),
It.IsAny(),
It.IsAny<Func<It.IsAnyType, Exception, string>>())

代码示例

TestOne类

internal sealed class TestOne
{
    private Mock<ILogger<ClassToTest>> _logger;
    private ClassToTest _classToTest;

    [OneTimeSetUp]
    public void OneTimeSetup()
    {
        _logger = new();

        _classToTest = new(_logger.Object);
    }

    [Test]
    public void StartAsync_CannotContinue_LogInformation()
    {
        // Act
        _classToTest.StartAsync(It.IsAny<CancellationToken>());

        // Assert
        _logger.Verify(logger => logger.Log(
            It.Is<LogLevel>(logLevel => logLevel == LogLevel.Information),
            It.IsAny<EventId>(),
            It.IsAny<It.IsAnyType>(),
            It.IsAny<Exception>(),
            It.IsAny<Func<It.IsAnyType, Exception, string>>()),
        Times.Once);
    }
}

TestTwo类

internal sealed class TestTwo
{
    private Mock<ILogger<AnotherClassToTest>> _logger;
    private AnotherClassToTest _anotherClassToTest;

    [OneTimeSetUp]
    public void OneTimeSetup()
    {
        _logger = new();

        _anotherClassToTest = new(_logger.Object);
    }

    [Test]
    public void SomeMethod_IsSuccessfull_LogInfo()
    {
        // Act
        _anotherClassToTest.SomeMethod(It.IsAny<string>(), It.IsAny<CancellationToken>());

        // Assert
        _logger.Verify(logger => logger.Log(
            It.Is<LogLevel>(logLevel => logLevel == LogLevel.Information),
            It.IsAny<EventId>(),
            It.IsAny<It.IsAnyType>(),
            It.IsAny<Exception>(),
            It.IsAny<Func<It.IsAnyType, Exception, string>>()),
        Times.Once);
    }
}

解决方案

1. 替换[OneTimeSetup]为[SetUp]

[OneTimeSetup]会在测试类所有测试执行前仅初始化一次,而IHostedService结合Timer会在后台持续运行,容易导致跨测试的状态污染。改用[SetUp],每个测试方法执行前都重新创建模拟对象和被测实例,确保测试完全隔离:

修改TestOne的初始化方法:

[SetUp]
public void Setup()
{
    _logger = new();
    _classToTest = new(_logger.Object);
}

TestTwo同理替换[OneTimeSetUp]为[SetUp]。

2. 手动终止HostedService的Timer和后台任务

在测试的[TearDown]方法中添加逻辑,停止Timer并释放资源,避免后台线程持续运行导致的无限循环或状态残留:

如果ClassToTest暴露了停止Timer的方法:

[TearDown]
public void TearDown()
{
    _classToTest.StopTimer(); // 自定义方法,停止Timer并释放资源
}

如果没有暴露方法,可通过反射获取Timer实例并停止:

[TearDown]
public void TearDown()
{
    var timerField = typeof(ClassToTest).GetField("_timer", BindingFlags.NonPublic | BindingFlags.Instance);
    if (timerField != null)
    {
        var timer = (Timer)timerField.GetValue(_classToTest);
        timer.Dispose();
    }
}

3. 精确匹配日志调用(可选)

若日志验证仍失败,可通过匹配日志消息模板的方式,避免泛型匹配的歧义:

_logger.Verify(
    x => x.Log(
        LogLevel.Information,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("你的日志消息关键词")),
        It.IsAny<Exception>(),
        It.IsAny<Func<It.IsAnyType, Exception, string>>()),
    Times.Once);

原因说明

  • [OneTimeSetup]的实例复用导致IHostedService的Timer在后台持续运行,跨测试时状态未重置,模拟对象的调用记录被干扰。
  • 调试模式下测试执行速度较慢,后台Timer的执行时机与测试断言时机冲突,导致断言时日志还未被调用。
  • 未及时释放Timer资源,后台线程无法终止,出现无限运行的情况。

内容的提问来源于stack exchange,提问作者Luis Leuppi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 13:52:17