预期对模拟调用一次,但为 0 次,使用 Func(T, TResult)
Expected invocation on the mock once, but was 0 times, with Func(T, TResult)
我似乎遇到了 Mock.Verify 的问题,认为某个方法没有被调用,但我可以完全验证它是。
单元测试:
[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);
(明确指定 TFileFunctionParameter
和 TFileFunctionOutput
)
这似乎有助于确定最小起订量验证匹配的类型。
@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 两个类型参数,TFileFunctionParameter
和 TFileFunctionOutput
, 和 五个普通参数 filePaths
、fileFunction
、fileFunctionParameter
、errorAction
和 outputs
.
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
期望的类型参数。
不过感觉把test和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);
(类型推断将从中给出正确的 TFileFunctionParameter
和 TFileFunctionOutput
)。
但是:您将测试 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
(即没有丑陋的反射)。
我似乎遇到了 Mock.Verify 的问题,认为某个方法没有被调用,但我可以完全验证它是。
单元测试:
[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);
(明确指定 TFileFunctionParameter
和 TFileFunctionOutput
)
这似乎有助于确定最小起订量验证匹配的类型。
@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 两个类型参数,TFileFunctionParameter
和 TFileFunctionOutput
, 和 五个普通参数 filePaths
、fileFunction
、fileFunctionParameter
、errorAction
和 outputs
.
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
期望的类型参数。
不过感觉把test和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);
(类型推断将从中给出正确的 TFileFunctionParameter
和 TFileFunctionOutput
)。
但是:您将测试 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
(即没有丑陋的反射)。