1

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.

Runnable version from Git

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?

m.edmondson
  • 30,382
  • 27
  • 123
  • 206
  • @NikolaiDante - Runnable version now available on Git https://github.com/medmondson/Moq-Func-Issue – m.edmondson Jan 22 '16 at 15:59
  • You completely hide the fact that your method `void IterateFiles(IEnumerable filePaths, Func fileFunction, TFileFunctionParameter fileFunctionParameter, FileIterationErrorAction errorAction, out List outputs);` is generic and requires two type parameters `TFileFunctionParameter` and `TFileFunctionOutput`. You should find out what types are substituted (inference) for `TFileFunctionParameter` and `TFileFunctionOutput` when you call the method in `Runner`. – Jeppe Stig Nielsen Jan 24 '16 at 09:31
  • ... (continued) Then you need to write your `It.IsAny<>()` expressions (inside the `Verify`) in a way that reproduces these inferred types. To find the values of `TFileFunctionParameter` and `TFileFunctionOutput` used inside `Runner`, it should be enough to "hover" the mouse over that method call in Visual Studio and read the types. If it is hard to read, check the generated IL. What version of the C# compiler and the .NET runtime (CLR) do you use? – Jeppe Stig Nielsen Jan 24 '16 at 09:34
  • @JeppeStigNielsen - Thanks, you're spot on although unfortunately NikolaiDante (answer below) beat you to it. Many thanks for taking the time to look though. – m.edmondson Jan 25 '16 at 09:05
  • What I do not understand is that his answer modifies the type parameters inside the `runner` which might be OK, but we expect Moq to be able to mock this without changing the code in `runner`. – Jeppe Stig Nielsen Jan 25 '16 at 09:33
  • @JeppeStigNielsen - Good point I didn't quite think as far as that. A bug in Moq? – m.edmondson Jan 25 '16 at 09:36
  • Easy come, easy go. I guess I'll have to try harder today to claw back the 15 pts! ;-\ – NikolaiDante Jan 26 '16 at 10:46
  • @NikolaiDante - Really sorry! Your answer did solve my initial problem and many thanks for taking the time to pull the repo to look at it properly, but looking at Jeppe's answer I couldn't not mark that as answer :-) – m.edmondson Jan 26 '16 at 10:59

2 Answers2

1

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

NikolaiDante
  • 18,469
  • 14
  • 77
  • 117
1

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.

Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181
  • And see [Mocking generic method call for any given type parameter](http://stackoverflow.com/questions/5311023/) for a vaguely related thread where it is clear that there is no "it-is-any" for type arguments. – Jeppe Stig Nielsen Jan 26 '16 at 09:51
  • Wow - excellent answer! Thank you very much. I've now marked this as answer since it more comprehensibly answers my question. – m.edmondson Jan 26 '16 at 10:43