【问题标题】:Expected invocation on the mock once, but was 0 times, with Func(T, TResult)预期在模拟上调用一次,但为 0 次,使用 Func(T, TResult)
【发布时间】:2016-01-22 13:55:46
【问题描述】:

我似乎在使用 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&lt;T&gt;,因此传递的参数无关紧要(out 参数除外——我的理解是这不能被模拟)。然而,Moq 不同意该方法被调用。

任何想法我哪里出错了?

【问题讨论】:

  • @NikolaiDante - 现在可以在 Git 上运行可运行版本 github.com/medmondson/Moq-Func-Issue
  • 您完全隐藏了您的方法void IterateFiles&lt;TFileFunctionParameter, TFileFunctionOutput&gt;(IEnumerable&lt;string&gt; filePaths, Func&lt;string, TFileFunctionParameter, TFileFunctionOutput&gt; fileFunction, TFileFunctionParameter fileFunctionParameter, FileIterationErrorAction errorAction, out List&lt;TFileFunctionOutput&gt; outputs); 是通用的并且需要两个类型参数TFileFunctionParameterTFileFunctionOutput 的事实。当您调用Runner 中的方法时,您应该找出TFileFunctionParameterTFileFunctionOutput 被替换(推断)的类型。
  • ...(续)然后您需要以重现这些推断类型的方式编写您的It.IsAny&lt;&gt;() 表达式(在Verify 内)。要查找Runner 中使用的TFileFunctionParameterTFileFunctionOutput 的值,只需将鼠标“悬停”在Visual Studio 中的该方法调用上并读取类型即可。如果难以阅读,请检查生成的 IL。您使用什么版本的 C# 编译器和 .NET 运行时 (CLR)?
  • @JeppeStigNielsen - 谢谢,虽然不幸的是 NikolaiDante(下面的答案)击败了你,但你已经成功了。非常感谢您花时间查看。
  • 我不明白的是,他的回答修改了 runner 中的类型参数,这可能没问题,但我们希望 Moq 能够在不更改 runner 中的代码的情况下模拟这一点。

标签: c# unit-testing moq


【解决方案1】:

NikolaiDante 的回答连同它下面的 cmets,基本上给出了解释。不过,既然我研究了一下,我会尽量写清楚。

您的问题完全没有显示出问题的主要原因,即该方法是一个通用方法。我们必须转到您链接的 Git 文件才能找到相关信息。

IFileService中声明的方法是:

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

要调用它,必须指定 both 两个类型参数,TFileFunctionParameterTFileFunctionOutput五个普通参数 filePaths,@987654327 @、fileFunctionParametererrorActionoutputs

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

所以在你的 Runner 类中,调用的真正含义是:

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

注意第一行的两个两种类型,注意方法组ProcessFile实际上变成了Func&lt;string, CsvParam, ProcessOutput&gt;,即使方法签名看起来更像Func&lt;string, ICsvConversionProcessParameter, ProcessOutput&gt;。可以从这样的方法组创建委托。 (并且Func&lt;in T1, in T2, out TResult&gt;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&lt;,,&gt; 具有另一种类型。所以这种解释你的问题。

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

但感觉更合适的是两个更改测试并保持runner代码不变。所以我们在测试中想要的是:

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)。

但是:您已将测试类放在 Runner 类之外的另一个项目/程序集中。 CsvParam 的类型是 internal 到它的程序集。所以你真的需要让CsvParam 在我的解决方案中可供测试使用。

您可以通过使类public 或通过包含属性使测试程序集成为MoqIssue 程序集的“朋友程序集”来使CsvParam 可访问:

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("MoqIssueTest")]

在属于 MoqIssue 项目的某个文件中。

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

【讨论】:

【解决方案2】:

基本上,问题在于Verify 中的某些内容与运行时的内容不完全匹配(它可能非常变化无常)。

我可以通过将Runner 中的代码更改为:

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

(明确指定 TFileFunctionParameterTFileFunctionOutput

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

正如@Lukazoid 所说的比我好得多,“Moq 将 DoSomething 视为与 DoSomething 不同的方法。”


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

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

  • 我可以看到的另一个潜在差异是 string[]IEnumerable&lt;string&gt;

  • List&lt;ProcessOutput&gt; 作为输出参数

【讨论】:

  • Process file 可分配给Func&lt;string, CsvConversionProcessParameter, ProcessOutput&gt; 因此我认为Is.IsAny&lt;Func&lt;string, ICsvConversionProcessParameter, ProcessOutput 应该匹配是不是正确的?
  • 我不确定 assignable 是否足以使其成为匹配项。如果您暂时将其公开并更改验证,那么测试是否通过?
  • 我已将其公开(无济于事),您建议如何更改验证?
  • 你在方法中通过ProcessFile进行测试,那么在验证中也这样做吗?
  • 我已经用你的建议更新了我上面的问题,不幸的是没有任何不同。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-15
  • 1970-01-01
  • 1970-01-01
  • 2016-05-03
  • 1970-01-01
相关资源
最近更新 更多