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

如何对依赖DispatcherTimer的SUT进行单元测试?求有效新方案

单元测试依赖DispatcherTimer的类的优雅方案

我已经找解决方案3天了,找到一篇相关文章但里面用的库已经停用了。现在求助新的或更优雅的单元测试依赖DispatcherTimer的类的方案。

被测系统(SUT)代码

public class RemainingTimeChangedEventArgs : EventArgs
{
    public TimeSpan RemainingTime { get; init; }
    public TimeSpan TimerSetFor { get; init; }
}

public class ExtendedTimer
{
    public event EventHandler<RemainingTimeChangedEventArgs>? RemainingTimeChanged;
    private readonly DispatcherTimer _timer;
    private TimeSpan _timerSetFor = TimeSpan.Zero;
    private TimeSpan _remainingTime = TimeSpan.Zero;
    
    public TimeSpan Interval
    {
        get => _timer.Interval;
        set => _timer.Interval = value;
    }
    
    public ExtendedTimer()
    {
        _timer = new();
        _timer.Tick += OnTimeChanged;
        _timer.Interval = TimeSpan.FromSeconds(1);
    }

    public void Initialize(TimeSpan timerSetFor)
    {
        _timerSetFor = timerSetFor;
        _remainingTime = timerSetFor;
    }

    public void Start()
    {
        _timer.Start();
    }

    public void Resume()
    {
        _timer.Start();
    }

    public void Pause()
    {
        _timer.Stop();
    }
    public void Stop()
    {
        _timer.Stop();
    }

    private void OnTimeChanged(object? sender, EventArgs e)
    {
        _remainingTime -= Interval;

        if (_remainingTime == TimeSpan.Zero) _timer.Stop();

        RemainingTimeChanged?.Invoke(this, new RemainingTimeChangedEventArgs
        {
            RemainingTime = _remainingTime,
            TimerSetFor = _timerSetFor
        });
    }
    
    ~ExtendedTimer() {
        _timer.Tick -= OnTimeChanged;
    }
}

该类初始化并启动后,会在每个时间间隔触发RemainingTimeChanged事件告知剩余时间,当剩余时间为0时停止触发事件。

当前测试代码(逻辑失败)

public class ExtendedTimerTests
{
    private readonly ExtendedTimer _sut = new();

    [Theory]
    [InlineData(10, 100, 10)]
    [InlineData(5, 50, 5)]
    [InlineData(20, 30, 1)]
    public async Task Start_GiveCorrectRemainingTimeEveryIntervalElapsed(
        int interval, int timerSetFor, int expectedExecutedFor)
    {
        _sut.Interval = TimeSpan.FromMilliseconds(interval);

        var ticksCount = 0;

        void OnSutOnRemainingTimeChanged(object? sender, RemainingTimeChangedEventArgs args)
        {
            ticksCount++;
        }

        _sut.RemainingTimeChanged += OnSutOnRemainingTimeChanged;

        _sut.Initialize(TimeSpan.FromMilliseconds(timerSetFor));
         _sut.Start();

         await Task.Delay(TimeSpan.FromMilliseconds(timerSetFor));
         Assert.Equal(expectedExecutedFor, ticksCount);


         // 确认等待额外时间后计数不变
         await Task.Delay(TimeSpan.FromMilliseconds(50));
         Assert.Equal(expectedExecutedFor, ticksCount);
    }

}

解决方案建议

1. 抽象Timer依赖,实现依赖注入

这是最优雅的方案,通过抽象隔离具体的DispatcherTimer,让测试可以自由替换实现:

首先定义Timer抽象接口:

public interface ITimer
{
    event EventHandler? Tick;
    TimeSpan Interval { get; set; }
    void Start();
    void Stop();
}

然后实现适配真实DispatcherTimer的包装类:

public class DispatcherTimerWrapper : ITimer
{
    private readonly DispatcherTimer _timer;

    public event EventHandler? Tick
    {
        add => _timer.Tick += value;
        remove => _timer.Tick -= value;
    }

    public TimeSpan Interval
    {
        get => _timer.Interval;
        set => _timer.Interval = value;
    }

    public DispatcherTimerWrapper()
    {
        _timer = new DispatcherTimer();
    }

    public void Start() => _timer.Start();
    public void Stop() => _timer.Stop();
}

修改ExtendedTimer构造函数,注入ITimer:

public class ExtendedTimer
{
    // ... 其他成员保持不变 ...
    
    // 替换原无参构造为依赖注入版本
    public ExtendedTimer(ITimer timer)
    {
        _timer = timer;
        _timer.Tick += OnTimeChanged;
        _timer.Interval = TimeSpan.FromSeconds(1);
    }

    // ... 其他方法保持不变 ...
}

2. 测试时使用模拟Timer(手动触发Tick)

使用Moq等模拟库,手动触发Tick事件,无需等待真实时间,测试速度快且稳定:

using Moq;

public class ExtendedTimerTests
{
    private readonly Mock<ITimer> _mockTimer;
    private readonly ExtendedTimer _sut;

    public ExtendedTimerTests()
    {
        _mockTimer = new Mock<ITimer>();
        _sut = new ExtendedTimer(_mockTimer.Object);
    }

    [Theory]
    [InlineData(10, 100, 10)]
    [InlineData(5, 50, 5)]
    [InlineData(20, 30, 1)]
    public void Start_GiveCorrectRemainingTimeEveryIntervalElapsed(
        int intervalMs, int timerSetForMs, int expectedTicks)
    {
        // 配置测试参数
        var interval = TimeSpan.FromMilliseconds(intervalMs);
        _sut.Interval = interval;
        var ticksCount = 0;

        _sut.RemainingTimeChanged += (_, _) => ticksCount++;
        _sut.Initialize(TimeSpan.FromMilliseconds(timerSetForMs));

        // 启动Timer并手动触发预期次数的Tick
        _sut.Start();
        for (int i = 0; i < expectedTicks; i++)
        {
            _mockTimer.Raise(t => t.Tick += null, EventArgs.Empty);
        }

        // 验证触发次数正确
        Assert.Equal(expectedTicks, ticksCount);
        // 验证剩余时间为0时Timer已停止
        _mockTimer.Verify(t => t.Stop(), Times.Once);

        // 再次触发Tick,确认计数不再增加
        _mockTimer.Raise(t => t.Tick += null, EventArgs.Empty);
        Assert.Equal(expectedTicks, ticksCount);
    }

    [Fact]
    public void Pause_StopsTimerImmediately()
    {
        _sut.Initialize(TimeSpan.FromSeconds(5));
        _sut.Start();
        
        _sut.Pause();
        
        _mockTimer.Verify(t => t.Stop(), Times.Once);
    }
}

3. 无法修改原代码时的补救方案

如果不能修改ExtendedTimer的代码,需要解决DispatcherTimer依赖UI线程Dispatcher的问题,同时优化计时逻辑:

public class ExtendedTimerTests
{
    private readonly ExtendedTimer _sut = new();
    private Dispatcher _dispatcher;

    public ExtendedTimerTests()
    {
        // 在测试线程创建并启动Dispatcher
        if (Dispatcher.CurrentDispatcher == null)
        {
            var thread = new Thread(() =>
            {
                _dispatcher = Dispatcher.CurrentDispatcher;
                Dispatcher.Run();
            });
            thread.IsBackground = true;
            thread.Start();
            while (_dispatcher == null) Thread.Sleep(10);
        }
        else
        {
            _dispatcher = Dispatcher.CurrentDispatcher;
        }
    }

    [Theory]
    [InlineData(10, 100, 10)]
    [InlineData(5, 50, 5)]
    [InlineData(20, 30, 1)]
    public async Task Start_GiveCorrectRemainingTimeEveryIntervalElapsed(
        int interval, int timerSetFor, int expectedExecutedFor)
    {
        _sut.Interval = TimeSpan.FromMilliseconds(interval);

        var ticksCount = 0;
        var completionSource = new TaskCompletionSource<bool>();

        void OnRemainingTimeChanged(object? sender, RemainingTimeChangedEventArgs args)
        {
            ticksCount++;
            if (args.RemainingTime == TimeSpan.Zero)
            {
                completionSource.TrySetResult(true);
            }
        }

        _sut.RemainingTimeChanged += OnRemainingTimeChanged;

        // 在Dispatcher线程执行Timer操作
        await _dispatcher.InvokeAsync(() =>
        {
            _sut.Initialize(TimeSpan.FromMilliseconds(timerSetFor));
            _sut.Start();
        });

        // 等待Timer完成或超时(增加缓冲时间避免计时误差)
        var completedTask = await Task.WhenAny(completionSource.Task, Task.Delay(timerSetFor + 100));
        Assert.Same(completionSource.Task, completedTask);

        Assert.Equal(expectedExecutedFor, ticksCount);

        // 再等待并处理剩余消息,确认计数不变
        await Task.Delay(50);
        await _dispatcher.InvokeAsync(() => {});
        Assert.Equal(expectedExecutedFor, ticksCount);
    }
}

内容的提问来源于stack exchange,提问作者Ihsan Nurul Iman

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 17:28:08