You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Xamarin.Forms单元测试报错:Expected invocation on the mock at least once

解决Xamarin.Forms NavigationService单元测试Verify失败的问题

嘿,我看你遇到的问题是单元测试里验证导航服务调用时失败了,报错说「期望的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 03:55:14