如何测试通过后台优先级调度器触发的CanExecuteChanged事件?
解决xUnit测试WPF BaseCommand的RaiseCanExecuteChanged事件延迟问题
问题背景
我正在学习xUnit,为现有库中实现ICommand的BaseCommand<T>抽象类编写全覆盖测试。这个类采用常见模式,将CanExecuteChanged事件路由至CommandManager.RequerySuggested,核心代码如下:
public abstract class BaseCommand<T> : ICommand { public event EventHandler? CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } public bool CanExecute(object? parameter) { return CanExecute((T?)parameter); } public virtual bool CanExecute(T? parameter) { return true; } public void Execute(object? parameter) { Execute((T?)parameter); } public abstract void Execute(T? parameter); public void RaiseCanExecuteChanged() { CommandManager.InvalidateRequerySuggested(); } }
测试过程中遇到两个问题:
- xUnit的
Assert.Raises*系列方法仅支持泛型EventHandler<T>,无法直接用于非泛型EventHandler,只能用自定义标记变量的方式编写测试。 - 调用
RaiseCanExecuteChanged后,CommandManager通过Background优先级调度器触发事件,导致测试中断言执行时,事件处理程序尚未被调用,测试失败。
解决方案
方法1:强制Dispatcher处理待调度任务
WPF的Dispatcher负责处理CommandManager的调度任务,测试中可以强制让Dispatcher处理完所有Background优先级的待处理任务,确保事件触发逻辑执行完毕:
[Fact] public void TestRaiseCanExecuteChanged() { var sut = new TestCommand(); bool changed = false; sut.CanExecuteChanged += (_, _) => changed = true; sut.RaiseCanExecuteChanged(); // 强制Dispatcher处理完所有Background级别的待处理任务 Dispatcher.CurrentDispatcher.Invoke(() => {}, DispatcherPriority.Background); Assert.True(changed); }
方法2:异步测试等待Dispatcher完成
如果项目支持异步测试,可以采用异步写法,通过Dispatcher.InvokeAsync等待任务完成,更符合xUnit的异步测试规范:
[Fact] public async Task TestRaiseCanExecuteChanged_Async() { var sut = new TestCommand(); bool changed = false; sut.CanExecuteChanged += (_, _) => changed = true; sut.RaiseCanExecuteChanged(); // 等待Dispatcher处理完所有Background任务 await Dispatcher.CurrentDispatcher.InvokeAsync(() => {}, DispatcherPriority.Background); Assert.True(changed); }
额外适配:兼容xUnit的Assert.Raises逻辑
如果想复用xUnit的断言风格,可以自行封装一个适配非泛型EventHandler的断言方法:
public static void RaisesNonGeneric(Action<EventHandler> subscribe, Action<EventHandler> unsubscribe, Action trigger) { bool raised = false; EventHandler handler = (_, _) => raised = true; subscribe(handler); try { trigger(); // 同样需要等待Dispatcher处理事件 Dispatcher.CurrentDispatcher.Invoke(() => {}, DispatcherPriority.Background); Assert.True(raised); } finally { unsubscribe(handler); } }
测试时调用该方法即可:
[Fact] public void TestRaiseCanExecuteChanged_WithCustomAssert() { var sut = new TestCommand(); RaisesNonGeneric( h => sut.CanExecuteChanged += h, h => sut.CanExecuteChanged -= h, () => sut.RaiseCanExecuteChanged() ); }
内容的提问来源于stack exchange,提问作者Oliver Giesen
相关产品推荐
相关产品推荐

