Xamarin.Forms单元测试报错:Expected invocation on the mock at least once
嘿,我看你遇到的问题是单元测试里验证导航服务调用时失败了,报错说「期望的mock调用至少执行一次,但从未发生」对吧?咱们来一步步拆解问题,然后搞定它。
问题根源
先对比你的ViewModel代码和测试代码:
- 在
OnObservationsCommandAsync里,你手动新建了一个ObservationDetailsParameter实例,把DisplayedCage和选中的Dossiers赋值进去,再传给导航服务。 - 但你的测试里,你创建了一个
Mock<IObservationDetailsParameter>对象,然后Verify的时候用这个mock对象去匹配调用参数——这就错了!ViewModel里根本没用到这个mock的参数,而是自己new了一个全新的实例。Moq默认是按引用相等来匹配参数的,这两个对象不是同一个,自然会判定「调用没发生」。
另外还有个小细节:你的ObservationsCommand是异步命令,直接调用Execute(null)是同步执行,可能会导致异步操作还没完成就跑Verify,也可能引发问题。
解决方案
这里给你几个可行的解决办法,按推荐程度排序:
1. 用It.Is<>匹配参数的属性值(最推荐)
我们不需要纠结参数是不是同一个实例,只要它的属性值符合预期就行。修改你的Verify代码:
// 替换原来的Verify语句 mockNavigationService.Verify(s => s.NavigateToAsync<ObservationDetailsViewModel>( It.Is<ObservationDetailsParameter>(p => p.Cage.Id == _displayedCage.Id && p.Dossiers.Cast<Dossier>().First().Id == _dossier1.Id )), Times.Once);
同时把测试方法改成异步的,确保异步命令执行完成:
[Fact] public async Task TestOnObservationsCommandAsync() { // ... 你的初始化代码 ... // Act:异步执行命令 await (Task)cageObsViewModel.ObservationsCommand.Execute(null); // Verify:用属性匹配判断调用是否发生 mockNavigationService.Verify(s => s.NavigateToAsync<ObservationDetailsViewModel>( It.Is<ObservationDetailsParameter>(p => p.Cage.Id == _displayedCage.Id && p.Dossiers.Cast<Dossier>().First().Id == _dossier1.Id )), Times.Once); }
还要记得删掉测试里那个多余的Mock<IObservationDetailsParameter>——ViewModel根本没用到它,留着反而混淆逻辑。
2. 让ObservationDetailsParameter支持值相等比较
如果这个类是你自己定义的,可以让它实现IEquatable<ObservationDetailsParameter>,这样Moq会按值来匹配参数,而不是引用:
public class ObservationDetailsParameter : IEquatable<ObservationDetailsParameter> { public Cage Cage { get; set; } public IEnumerable<Dossier> Dossiers { get; set; } public bool Equals(ObservationDetailsParameter other) { if (other == null) return false; // 按属性值比较,这里根据你的实际需求调整判断逻辑 return Cage.Id == other.Cage.Id && Dossiers.Select(d => d.Id).SequenceEqual(other.Dossiers.Select(d => d.Id)); } public override bool Equals(object obj) => Equals(obj as ObservationDetailsParameter); public override int GetHashCode() => HashCode.Combine(Cage.Id, Dossiers.Select(d => d.Id)); }
然后测试里创建一个真实的预期参数,直接用它来Verify:
// 初始化代码里创建预期参数 var expectedParameter = new ObservationDetailsParameter { Cage = _displayedCage, Dossiers = _dossierList.SelectedItems }; // Act之后 mockNavigationService.Verify(s => s.NavigateToAsync<ObservationDetailsViewModel>(expectedParameter), Times.Once);
3. 抽离参数创建逻辑(可选,适合复杂场景)
如果你的参数创建逻辑很复杂,可以把这部分抽成一个工厂接口,比如IObservationDetailsParameterFactory,然后在ViewModel里注入这个工厂,测试时mock工厂返回我们预期的参数。不过这个方式有点过度设计,除非必要,否则不推荐。
额外的小修正
测试里的CageObsViewModel构造函数,你传入了mockCageDetailsService等依赖,确保这些依赖不会干扰导航逻辑——比如它们的方法不会抛出异常,否则命令执行会中断,导航调用也不会发生。
内容的提问来源于stack exchange,提问作者gogs

