Reputation: 30862
I appear to be having an issue with Mock.Verify that believes a method wasn't called but I can fully verify that it is.
Unit test:
[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);
}
Alternative Unit test: (after comment below)
[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();
}
}
When I debug I can see that service.IterateFiles
is being called. In addition as all parameters are marked with It.IsAny<T>
the arguments passed don't matter (with the exception of the out parameter - my understand is this cannot be mocked). Yet Moq disagrees the method is called.
Any ideas where I'm going wrong?
Upvotes: 1
Views: 1453
Reputation: 61912
NikolaiDante's answer together with the comments below it, essentially gives the explanation. Still, since I have investigated it a bit, I will try to write it clearly.
Your question entirely fails to show the main cause of your problem which is that the method is a generic one. We had to go to the Git files you link, to find out about that.
The method as declared in IFileService
is:
void IterateFiles<TFileFunctionParameter, TFileFunctionOutput>(
IEnumerable<string> filePaths,
Func<string, TFileFunctionParameter, TFileFunctionOutput> fileFunction,
TFileFunctionParameter fileFunctionParameter,
FileIterationErrorAction errorAction,
out List<TFileFunctionOutput> outputs);
To call it, one has to specify both the two type arguments, TFileFunctionParameter
and TFileFunctionOutput
, and the five ordinary arguments filePaths
, fileFunction
, fileFunctionParameter
, errorAction
, and outputs
.
C# is helpful and offers type inference with which we do not have to write the type arguments in the source code. The compiler figures which type arguments we want. But the two type arguments are still there, only "invisible". To see them, either hold your mouse over the generic method call below (and the Visual Studio IDE will show you them), or look at the output IL.
So inside your Runner
class, the call really means:
service.IterateFiles<CsvParam, ProcessOutput>(paths,
(Func<string, CsvParam, ProcessOutput>)ProcessFile,
new CsvParam(), FileIterationErrorAction.ContinueThenThrow, out output);
Pay attention two the two types in the first line, and note that the method group ProcessFile
is actually turned into a Func<string, CsvParam, ProcessOutput>
even if the methods signature looks more like Func<string, ICsvConversionProcessParameter, ProcessOutput>
. Delegates can be created from methods groups like that. (And it is not really relevant that Func<in T1, in T2, out TResult>
is marked as contravariant in T2
.)
If we inspect your Verify
, then we see that type inference really sees it as:
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);
So Moq cannot really verify that this is called, since the call uses a different first type argument, and also the fileFunction
Func<,,>
has another type. So this kind of explains you problem.
NikolaiDante shows how you can change runner
to actually use the type arguments that your Verify
expects.
But it feels more appropriate two change the test and keep the runner
code unchanged. So what we want in the test, is:
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);
(type inference will give the correct TFileFunctionParameter
and TFileFunctionOutput
from this).
However: You have put your test class in another project/assembly than the Runner
class. And the type CsvParam
is internal
to its assembly. So you really need to make CsvParam
accessible to the test in my solution.
You can make CsvParam
accessible either by making the class public
, or by making the test assembly a "friend assembly" of the MoqIssue assembly by including the attribute:
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MoqIssueTest")]
in some file belonging to the MoqIssue project.
Note that the Moq framework has no problems with an internal
type, so you do not have to turn any of Moq's assemblies into "friends" for this. It is only required to express the Verify
easily (i.e. without ugly reflection) in your MoqIssueTest assembly.
Upvotes: 1
Reputation: 18639
Basically, the problem is that something in the Verify
doesn't exactly match what is there at run-time (it can be quite fickle).
I was able to get it pass via changing the code in Runner
to:
service.IterateFiles<ICsvConversionProcessParameter, ProcessOutput>(paths, ProcessFile, new CsvParam(), FileIterationErrorAction.ContinueThenThrow, out output);
(Specifiying TFileFunctionParameter
and TFileFunctionOutput
explicitly)
Which seemed to help nail down the types for moq's verify to match.
As @Lukazoid said much better than I," Moq treats DoSomething as a different method to DoSomething."
Some candidates, since ruled out:
There seems to be a mismatch between Func<string, ICsvConversionProcessParameter, ProcessOutput>
and ProcessFile
as ProcessFile
doesn't seem to be defined as a func.
Another potential difference I can see is string[]
vs IEnumerable<string>
.
List<ProcessOutput>
as the out param
Upvotes: 1