m.edmondson
m.edmondson

Reputation: 30862

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

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?

Upvotes: 1

Views: 1453

Answers (2)

Jeppe Stig Nielsen
Jeppe Stig Nielsen

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

NikolaiDante
NikolaiDante

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

Related Questions