在运行测试时,Dispatcher.Invoke(Action) 会阻塞
编辑
尽管我很想把这个问题解决掉,但推荐的解决方案对我不起作用。相信我,我已经试过了。我的测试一直在转圈,一点反应也没有。我用过推荐的Push Frame方法,但它仍然不起作用。这意味着,测试现在在PushFrame调用处挂起。
Here is how my test looks like:
[Fact]
public void OpenPersonDetailsAndChangeTheirNameTest()
{
//A
var person = new Mock<IPersonViewModel>();
var pocoPersonMock = new Mock<POCO_Person>();
var windowServiceMoq = new Mock<IWindowService>();
var viewModelRepositoryMock = new Mock<IViewModelRepository>();
var containerRepositoryMock = new Mock<IContainerRepository>();
var providerMock = new Mock<IContainerProvider>();
var dispatcherServiceMock = new Mock<IDispatcherService>();
var designerServiceMock = new Mock<IDesignerService>();
var dispatcher = Dispatcher.CurrentDispatcher;
var dispatcherFrame = new DispatcherFrame();
RestMoq = new Mock<Models.Generated.Models>("", new System.Net.Http.HttpClient());
containerRepositoryMock.Setup(c => c.GetContainer()).Returns(providerMock.Object);
providerMock.Setup(p => p.Resolve(typeof(IWindowService))).Returns(windowServiceMoq.Object);
providerMock.Setup(p => p.Resolve(typeof(IViewModelRepository))).Returns(viewModelRepositoryMock.Object);
providerMock.Setup(p => p.Resolve(typeof(IPersonViewModel))).Returns(person.Object);
viewModelRepositoryMock.Setup(v => v.GetViewModel<IPersonViewModel>()).Returns(person.Object);
windowServiceMoq.Setup(s => s.Show(It.IsAny<IBaseViewModel>())).Verifiable();
designerServiceMock.Setup(d => d.InDesignMode()).Returns(true);
person.Setup(p => p.FirstName).Returns("Test First Name");
RestMoq
.Setup(s => s.PeopleAllAsync())
.ReturnsAsync(
() =>
new List<POCO_Person>
{
pocoPersonMock.Object
}
);
dispatcherServiceMock.Setup(d => d.GetDispatcher()).Returns(dispatcher);
var sut = new MainViewModel(RestMoq.Object, windowServiceMoq.Object, dispatcherServiceMock.Object, designerServiceMock.Object, viewModelRepositoryMock.Object, containerRepositoryMock.Object);
//A
//populate people
if (sut.PopulatePeopleCommand.CanExecute(null))
{
sut.PopulatePeopleCommand.Execute(null);
}
Dispatcher.PushFrame(dispatcherFrame);
sut.SelectedPerson = sut.People.FirstOrDefault();
if (sut.OpenPersonDetailsCommand.CanExecute(person.Object))
{
sut.OpenPersonDetailsCommand.Execute(person.Object);
}
//A
windowServiceMoq.Verify(s => s.Show(It.IsAny<IBaseViewModel>()), Times.Once());
providerMock.Verify(p => p.Resolve(typeof(IWindowService)), Times.AtLeastOnce());
}
我正在开发一个WPF应用,使用.NET 8.0,并且正在为它编写测试。应用和测试项目都使用.NET 8.0。现在在调度程序(Dispatcher)挂起并最终在运行测试时任务被取消时,我遇到了一个问题。我有一个IoC并且使用Prism注入我的服务。在我的BaseViewModel中,我有一个在UI线程上运行代码的方法,恰当地命名为 UI(),它看起来像这样:
public void UI(Action action)
{
Dispatcher.Invoke(action);
}
无论是在调试还是发布模式下运行应用,这个方法都能工作。这里是调度程序是如何创建的:
public Dispatcher GetDispatcher()
{
return Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
}
如你所见,这个实现考虑了应用不存在的情况,我以为找到了一个可行的方案。然而,当我用它来填充一个可观察集合时,在测试中却会一直挂起,而在运行或调试时却没问题。我束手无策,想知道在使用非应用调度程序时是否需要改为Push Frames。这里是它在命令中的使用方式:
private async void PopulatePeople()
{
Progress = 0;
IsBusy = true;
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));// <--- this line is causing issue
});
IsBusy = false;
}
我看不出这为什么会不起作用,但话又说回来,我对Dispatcher的了解也不够深入,无法猜测原因。任何建议都非常感谢,谢谢
编辑2
以下是我的DelegateCommand的实现方式:
public class DelegateCommand : IDelegateCommand
{
public event EventHandler CanExecuteChanged;
Action _execute;
Func<bool> _canExecute;
public DelegateCommand(Action action, Func<bool> canExecute = null)
{
_execute = action;
_canExecute = canExecute ?? (() => true);
}
public bool CanExecute(object parameter)
{
return _canExecute.Invoke();
}
public void Execute(object parameter)
{
_execute?.Invoke();
}
public void Refresh()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
以及接口:
public interface IDelegateCommand : ICommand
{
void Refresh();
}
解决方案
经过一番排查并试过Dispatcher.Yield() 等方法后,仍然没有效果,也就是执行了代码,但在调用UI(Action) 方法后从不返回。
这让我思考,主应用是基于Dispatcher的,它会旋转并向消息泵推送消息,但只要应用在运行,它就永远不会真正退出。由于我的测试在UI上没有任何展示,调度程序就像平常一样工作,呆在那里自旋。因此现在我要么重复应用程序的做法,要么在完成后弃用调度程序。这并不容易,因为调度程序就放在那里,只要我的调用是同步的,它们就永远不会被执行。
一个任务来救场。当我启动测试时,我也会启动一个任务,它会自旋并检查我的测试条件。在这种情况下,是视图模型中的People集合。看起来大致是这样的:
//create frame for our dispatcher
var dispatcherFrame = new DispatcherFrame(true);
//create a task to monitor criteria
var task = Task.Factory.StartNew(() => DispatcherFrame_Continue(sut, dispatcherFrame));
//... test code in here
//method for breaking out of dispatcher loop
void DispatcherFrame_Continue(MainViewModel vm, DispatcherFrame frame)
{
bool breakOut = false;
while (!breakOut)
{
if (vm.People != null && vm.People.Count > 0)
{
breakOut = true;
}
Thread.Sleep(200);
}
frame.Continue = false;
}
这个方案对我而言效果最佳,因为在使用调度程序时不需要对现有代码做修改,但会让测试变得有点冗长和复杂。因为这些条件会随着测试而变化,方法还得针对每个测试进行适配。也许有更好的解决方案,但我目前找不到。