如何对依赖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
相关产品推荐
相关产品推荐

