预期对模拟调用一次,但为 0 次,使用 Func(T, TResult)

Expected invocation on the mock once, but was 0 times, with Func(T, TResult)

我似乎遇到了 Mock.Verify 的问题,认为某个方法没有被调用,但我可以完全验证它是。

Runnable version from Git

单元测试:

[Test]
public void IterateFiles_Called()
{
     Mock<IFileService> mock = new Mock<IFileService>();
     var flex = new Runner(mock.Object);

     List<ProcessOutput> outputs;
     mock.Verify(x => x.IterateFiles(It.IsAny<IEnumerable<string>>(),
                    It.IsAny<Func<string, ICsvConversionProcessParameter, ProcessOutput>>(),
                    It.IsAny<ICsvConversionProcessParameter>(),
                    It.IsAny<FileIterationErrorAction>(),
                    out outputs), Times.Once);

        }

替代单元测试:(在下方评论后)

[Test]
public void IterateFiles_Called()
{
     Mock<IFileService> mock = new Mock<IFileService>();
     var flex = new Runner(mock.Object);

     List<ProcessOutput> outputs;
     mock.Verify(x => x.IterateFiles(It.IsAny<string[]>(),
                        flex.ProcessFile, //Still fails
                        It.IsAny<ICsvConversionProcessParameter>(),
                        It.IsAny<FileIterationErrorAction>(),
                        out outputs), Times.Once);

}

Runner.cs:

public class Runner
    {
        public Runner(IFileService service)
        {
            string[] paths = new[] {"path1"};

            List<ProcessOutput> output = new List<ProcessOutput>();

            service.IterateFiles(paths, ProcessFile, new CsvParam(), FileIterationErrorAction.ContinueThenThrow, out output);
        }

        public ProcessOutput ProcessFile(string file, ICsvConversionProcessParameter parameters)
        {
            return new ProcessOutput();
        }
    }

当我调试时,我可以看到正在调用 service.IterateFiles。此外,由于所有参数都标有 It.IsAny<T>,因此传递的参数无关紧要(out 参数除外——我的理解是这不能被模拟)。然而 Moq 不同意调用该方法。

有什么地方出错了吗?

基本上,问题是 Verify 中的某些内容与 run-time 中的内容不完全匹配(它可能非常善变)。

我能够通过将 Runner 中的代码更改为:

来让它通过
service.IterateFiles<ICsvConversionProcessParameter, ProcessOutput>(paths, ProcessFile, new CsvParam(), FileIterationErrorAction.ContinueThenThrow, out output);

(明确指定 TFileFunctionParameterTFileFunctionOutput

这似乎有助于确定最小起订量验证匹配的类型。

@Lukazoid 说得比我好得多,"Moq 将 DoSomething 视为与 DoSomething 不同的方法。"


一些候选人,因为被排除:

  • Func<string, ICsvConversionProcessParameter, ProcessOutput>ProcessFile 之间似乎不匹配,因为 ProcessFile 似乎没有被定义为函数。

  • 我能看到的另一个潜在差异是 string[]IEnumerable<string>

  • List<ProcessOutput> 作为输出参数

NikolaiDante 的回答及其下方的评论基本上给出了解释。不过,既然调查了一下,还是尽量写清楚。

您的问题完全没有说明问题的主要原因,即该方法是 generic 方法。我们不得不去你 link 的 Git 文件来了解一下。

IFileService中声明的方法是:

void IterateFiles<TFileFunctionParameter, TFileFunctionOutput>(
    IEnumerable<string> filePaths,
    Func<string, TFileFunctionParameter, TFileFunctionOutput> fileFunction,
    TFileFunctionParameter fileFunctionParameter,
    FileIterationErrorAction errorAction,
    out List<TFileFunctionOutput> outputs);

要调用它,必须指定 both 两个类型参数,TFileFunctionParameterTFileFunctionOutput 五个普通参数 filePathsfileFunctionfileFunctionParametererrorActionoutputs.

C# 很有帮助,它提供了类型推断,我们不必在源代码中编写类型参数。编译器计算出我们想要哪种类型的参数。但是两个类型参数还在,只有"invisible"。要查看它们,请将鼠标悬停在下面的通用方法调用上(Visual Studio IDE 将向您显示它们),或者查看输出 IL。

所以在你的 Runner class 里面,调用的真正意思是:

service.IterateFiles<CsvParam, ProcessOutput>(paths,
  (Func<string, CsvParam, ProcessOutput>)ProcessFile,
  new CsvParam(), FileIterationErrorAction.ContinueThenThrow, out output);

注意第一行的两个类型,注意方法组 ProcessFile 实际上变成了 Func<string, CsvParam, ProcessOutput> 即使方法签名看起来更像 Func<string, ICsvConversionProcessParameter, ProcessOutput> .委托可以从这样的方法组中创建。 (并且 Func<in T1, in T2, out TResult>T2 中被标记为逆变并不真正相关。)

如果我们检查您的 Verify,那么我们会看到类型推断确实将其视为:

mock.Verify(x => x.IterateFiles<ICsvConversionProcessParameter, ProcessOutput>(
  It.IsAny<IEnumerable<string>>(),
  It.IsAny<Func<string, ICsvConversionProcessParameter, ProcessOutput>>(),
  It.IsAny<ICsvConversionProcessParameter>(),
  It.IsAny<FileIterationErrorAction>(),
  out outputs), Times.Once);

所以 Moq 无法真正验证调用,因为调用使用了不同的第一类型参数,而且 fileFunction Func<,,> 有另一种类型。所以这种解释你的问题。

NikolaiDante 展示了如何更改 runner 以实际使用 Verify 期望的类型参数。

不过感觉把testrunner代码保持不变两个更合适。所以我们在测试中想要的是:

mock.Verify(x => x.IterateFiles(It.IsAny<IEnumerable<string>>(),
  It.IsAny<Func<string, CsvParam, ProcessOutput>>(),
  It.IsAny<CsvParam>(),
  It.IsAny<FileIterationErrorAction>(),
  out outputs), Times.Once);

(类型推断将从中给出正确的 TFileFunctionParameterTFileFunctionOutput)。

但是:您将测试 class 放在另一个 project/assembly 而不是 Runner class。类型 CsvParam 是其程序集的 internal 。所以你真的需要让 CsvParam 可以访问我的解决方案中的测试。

您可以通过使 class public 或通过包含属性使测试程序集成为 MoqIssue 程序集的 "friend assembly" 来使 CsvParam 可访问:

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("MoqIssueTest")]

在属于 MoqIssue 项目的一些文件中。

请注意,Moq 框架对于 internal 类型没有问题,因此您不必为此将任何 Moq 程序集转换为 "friends"。只需要在您的 MoqIssueTest 程序集中轻松表达 Verify(即没有丑陋的反射)。