【问题标题】:Moq saying 0 calls when it lists a call?Moq 列出呼叫时说 0 个呼叫?
【发布时间】:2020-04-20 23:04:10
【问题描述】:

我有一个使用 Moq 模拟接口和验证调用的单元测试。下面是测试代码:

[Fact]
public void NewBlank_InvokesManagerAdd()
{
    // ReSharper disable once AssignNullToNotNullAttribute
    var newPath = Path.Combine(_testSaveDirectory, "InvokeBlank.txt");

    _dbManagerMock.Setup(manager => manager.KeynoteDBs).Returns(new ObservableCollection<KeynoteDBVM>());
    _dialogMock.Setup(dialog => dialog.GetSaveFileDialogResult(It.IsAny<SaveFileDialogData>()))
               .Returns(newPath);

    _commands.CmdNewBlank.Execute(null);

    _dbManagerMock
        .Verify(manager => manager.AddDB(It.IsAny<KeynoteDB>(), It.IsAny<int>()),
                Times.Once);
}

但是,当我运行它时,我得到了这个测试失败:

Moq.MockException

Expected invocation on the mock once, but was 0 times: manager => manager.AddDB(It.IsAny<KeynoteDB>(), It.IsAny<int>())

Performed invocations:

   Mock<IDBManager:1> (manager):

      IDBManager.KeynoteDBs
      IDBManager.IsLoading = True
      IDBManager.ActiveDB
      IDBManager.AddDB(KeynoteDB, -1)
      IDBManager.ActiveDB
      IDBManager.IsLoading = False

   at Moq.Mock.Verify(Mock mock, LambdaExpression expression, Times times, String failMessage)
   at Moq.Mock`1.Verify[TResult](Expression`1 expression, Func`1 times)
   at KMCore_Tests.AppCommandsTests.NewBlank_InvokesManagerAdd() in *my path*\AppCommandsTests.cs:line 140

它不是在中间的调用列表中列出该调用 1 次吗?怎么说它有 0 次调用?我错过了什么?我觉得我一定错过了一些愚蠢的东西,但我看不到它......

编辑

好的,结果证明这是一个竞争条件问题,因为命令是异步的。命令执行调用了一个 async void 方法,并且它必须在实际调用之前已经击中断言(或者至少这是我能想到的全部)。我在调用执行之后和断言之前放入了一个 await Task.Delay(500),它现在正在通过。

有没有更好的方法来测试这种情况?这些命令本质上是按钮处理程序,因此根据我的理解,我认为 async void 在这里是正确的,但这意味着我不能在单元测试中等待它...

【问题讨论】:

标签: c# unit-testing moq


【解决方案1】:

你应该avoid using async void anyway

async void 表示一劳永逸。在您的情况下,您似乎不希望这样做,因为您需要等到异步方法完成。

如果您无法更改方法签名,您可以将async void 方法的主体移动到Task 返回方法。

例如,改变

public async void MyAsyncVoidMethod()
{
    await Task.Delay(500);

    MethodToBeCalled();
}

// Wait this task in unit test
public Task MyAsyncTask { get; private set; }

public async void MyAsyncVoidMethod()
{
    MyAsyncTask = MyAsyncTaskMethod();
    await MyAsyncTask;
}

public async Task MyAsyncTaskMethod()
{
    await Task.Delay(500);

    MethodToBeCalled();
}

或者,如果在命令完成后触发了任何事件,您也可以挂钩其中一个事件以在您的测试中保持通知。

【讨论】:

  • 我知道通常不推荐使用 async void,但我认为例外是像按钮处理程序这样的“顶级”方法,它们调用异步方法来获取数据并相应地更新 UI。这不是按钮处理程序,但它是 VM 上的 ICommand,所以它基本上做同样的事情。在主程序执行中,我确实希望它触发并忘记它,然后在它返回后更新 UI,等等。但是在单元测试中,我试图验证它是否调用了正确的方法来在它之后向数据模型添加东西回来。 ICommand 的执行方法不能是异步的(AFAIK)。
  • @sfaust 我不得不在使用大量ICommand 的UWP 应用程序中处理许多类似的场景。所以我明白这一点。这里的要点是虽然它在应用程序中是 fire-n-forget,但您仍然需要在测试中等待它,所以总的来说,您有混合 fire-n-forget(这不是真正的 fire-n-forget) .我等待这些行动的方式是我在答案中提到的。
  • 好的,那么代码在常规代码中?对不起,我在测试课上读它。所以基本上我的 VM 将在 Task 方法中有主要代码,但是 void 方法只会调用 task 方法,对吗?对于 ICommand 执行,它会调用 void 但单元测试会调用 Task 方法并等待它......这是有道理的,不应该太难,谢谢!
  • @sfaust 是的,在你的虚拟机中。这个想法是您必须公开一个任务以供单元测试等待。要么让单元测试直接调用任务返回方法,要么单元测试通过你的虚拟机的属性来获取任务,或者别的什么。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-06-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-12-27
  • 2012-01-31
  • 1970-01-01
相关资源
最近更新 更多