测试时Dispatcher.Invoke(Action)挂起,PushFrame方法无效求助
问题分析与解决建议
核心问题拆解
- 测试环境无WPF应用实例,
Application.Current为null,导致Dispatcher使用测试线程的Dispatcher,但测试线程无消息循环,Invoke调用直接阻塞。 async void的命令执行方法无法被测试代码追踪完成状态,导致手动调用的PushFrame无法正确终止,永久卡住。- 测试中未正确处理异步任务的完成时机,依赖未就绪的UI线程逻辑引发死锁。
具体解决步骤
1. 替换async void命令逻辑为async Task
async void在非UI场景(如测试)中会导致任务无法被等待,异常无法被捕获。修改PopulatePeople方法:
private async Task PopulatePeople() // 改为async Task { Progress = 0; IsBusy = true; try { await Task.Run(async () => { ShowToastMessage("About to get a load of records"); var restPeople = await peopleRestService.PeopleAllAsync(); ShowToastMessage("Got a load of records"); List<IBaseViewModel> localList = new List<IBaseViewModel>(); var pvm = Container.Resolve<IPersonViewModel>(); pvm.Person = pe; pvm.Title = pe.Name; pvm.RestPeople = new ObservableCollection<RestPerson>(restPeople.Select(p => new RestPerson(p))); localList.Add(pvm); GenerateExpensiveViewModels(numberOfPeople, maxVmIndex, vmIndex, localList, restPeople); ShowToastMessage("Got them all!"); UI(() => People = new ObservableCollection<IBaseViewModel>(localList)); }); } finally { IsBusy = false; } }
2. 实现支持异步的DelegateCommand
原DelegateCommand不支持异步方法,需扩展适配:
public class AsyncDelegateCommand : IDelegateCommand { public event EventHandler CanExecuteChanged; readonly Func<Task> _executeAsync; readonly Func<bool> _canExecute; public AsyncDelegateCommand(Func<Task> executeAsync, Func<bool> canExecute = null) { _executeAsync = executeAsync; _canExecute = canExecute ?? (() => true); } public bool CanExecute(object parameter) => _canExecute.Invoke(); public async void Execute(object parameter) // 保持void实现ICommand接口,内部await异步逻辑 { if (CanExecute(parameter)) await _executeAsync(); } public void Refresh() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); }
ViewModel中初始化命令:
PopulatePeopleCommand = new AsyncDelegateCommand(PopulatePeople);
3. 测试中Mock Dispatcher调用(最优方案)
测试场景无需依赖真实WPF Dispatcher,直接MockIDispatcherService跳过UI线程限制:
// 提前封装BaseViewModel的UI方法为依赖注入的服务 // BaseViewModel代码: // private readonly IDispatcherService _dispatcherService; // public BaseViewModel(IDispatcherService dispatcherService) => _dispatcherService = dispatcherService; // public void UI(Action action) => _dispatcherService.Invoke(action); // 测试中设置Mock:直接在当前线程执行Action dispatcherServiceMock.Setup(s => s.Invoke(It.IsAny<Action>())).Callback<Action>(action => action());
4. 测试中正确等待异步任务完成
通过TaskCompletionSource追踪命令执行状态,避免手动调用PushFrame:
[Fact] public async Task OpenPersonDetailsAndChangeTheirNameTest() // 改为async Task测试方法 { // ... 原有Mock代码保持不变 ... var tcs = new TaskCompletionSource<bool>(); // 拦截UI方法,任务完成时触发tcs dispatcherServiceMock.Setup(s => s.Invoke(It.IsAny<Action>())) .Callback<Action>(action => { action(); tcs.TrySetResult(true); }); var sut = new MainViewModel(RestMoq.Object, windowServiceMoq.Object, dispatcherServiceMock.Object, designerServiceMock.Object, viewModelRepositoryMock.Object, containerRepositoryMock.Object); // 执行命令并等待完成 if (sut.PopulatePeopleCommand.CanExecute(null)) { sut.PopulatePeopleCommand.Execute(null); await tcs.Task; // 等待异步任务完成 } sut.SelectedPerson = sut.People.FirstOrDefault(); if (sut.OpenPersonDetailsCommand.CanExecute(person.Object)) { sut.OpenPersonDetailsCommand.Execute(person.Object); } // 验证逻辑 windowServiceMoq.Verify(s => s.Show(It.IsAny<IBaseViewModel>()), Times.Once()); providerMock.Verify(p => p.Resolve(typeof(IWindowService)), Times.AtLeastOnce()); }
关键注意事项
- 测试中永远避免依赖真实WPF UI组件(如Dispatcher),Mock相关服务是最可靠的解耦方式。
- 命令执行逻辑优先使用
async Task而非async void,便于测试追踪任务状态。 - 不要在测试中手动调用
PushFrame,通过异步等待确保所有任务执行完毕。
内容的提问来源于stack exchange,提问作者XAMlMAX
相关产品推荐
相关产品推荐

