Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to verify function calls with params keyword #539

Closed
simonachmueller opened this issue Apr 10, 2019 · 3 comments
Closed

How to verify function calls with params keyword #539

simonachmueller opened this issue Apr 10, 2019 · 3 comments

Comments

@simonachmueller
Copy link

Question
I'm using ILogger from Microsoft.Extensions.Logging in my ASP.Net Core 2.2 code and I want to write a unit test where I can check if logger.LogError() was called or not.

In code it looks like:

SUT:

public class MyTestClass
{
    private readonly ILogger<MyTestClass> logger;
    public MyTestClass(ILogger<MyTestClass> logger)
    {
         this.logger = logger;
    }
    public void MyFunction(var url, StringContent payload)
    {
        //prepare web client..
        var response = await client.PostAsync(url, payload);
        if (!response.IsSuccessStatusCode)
        {
             var resultString = await response.Content.ReadAsStringAsync();
             logger.LogError($"Calling {url} was not successfull {response.ReasonPhrase}). Response was {resultString}");
        }
    }
}

And I want to write such test code:

[Test]
void MyTest()
{
    // prepare web client testable stub...
    // ....
    var loggerMock = Substitute.For<ILogger<MyTestClass>>();
    var sut = new MyTestClass(loggerMock);
    sut.MyFunction("https://someurl.com", new StringContent("somePayload"));
    loggerMock.DidNotReceive().LogError(Arg.Any<string>(), Arg.Any<object[]>());
}

The problem is that the LogError() function has following signature (with params keyword):
public static void LogError(this ILogger logger, string message, params object[] args);
and when I run my test I'm getting an error:

Message: NSubstitute.Exceptions.RedundantArgumentMatcherException : Some argument specifications (e.g. Arg.Is, Arg.Any) were left over after the last call.

This is often caused by using an argument spec with a call to a member NSubstitute does not handle (such as a non-virtual member or a call to an instance which is not a substitute), or for a purpose other than specifying a call (such as using an arg spec as a return value). For example:

    var sub = Substitute.For<SomeClass>();
    var realType = new MyRealType(sub);
    // INCORRECT, arg spec used on realType, not a substitute:
    realType.SomeMethod(Arg.Any<int>()).Returns(2);
    // INCORRECT, arg spec used as a return value, not to specify a call:
    sub.VirtualMethod(2).Returns(Arg.Any<int>());
    // INCORRECT, arg spec used with a non-virtual method:
    sub.NonVirtualMethod(Arg.Any<int>()).Returns(2);
    // CORRECT, arg spec used to specify virtual call on a substitute:
    sub.VirtualMethod(Arg.Any<int>()).Returns(2);

To fix this make sure you only use argument specifications with calls to substitutes. If your substitute is a class, make sure the member is virtual.

Another possible cause is that the argument spec type does not match the actual argument type, but code compiles due to an implicit cast. For example, Arg.Any<int>() was used, but Arg.Any<double>() was required.

NOTE: the cause of this exception can be in a previously executed test. Use the diagnostics below to see the types of any redundant arg specs, then work out where they are being created.

Diagnostic information:

Remaining (non-bound) argument specifications:
    any String
    any Object[]

All argument specifications:
    any String
    any Object[]
 

So my question is: how can I check received calls for functions with an params argument?

@dtchepak
Copy link
Member

LogError is a non-virtual extension method, so we can not reliably mock this with NSubstitute. Depending on its implementation, we might be able to get it to work. Looking at the source, it calls logger.Log(LogLevel.Error, message, args), which is itself an extension method. Eventually, it gets down to this call.

Which means we end up with something like this:

logger.DidNotReceive().Log(LogLevel.Error, 0, Arg.Any<FormattedLogValues>(), null, Arg.Any<Func<FormattedLogValues, Exception, string>>());

Rather than this, I'd suggest writing your own ILogger implementation that stores all messages logged, and you can assert on that. Maybe try TestLogger and its LogMessages property? It means losing NSubstitute syntax for some of these, but at least your test double will work as expected with the many extension methods defined for logging.

I also noticed MyFunction is a void type, but uses await. It may be better to make it return a task, so you can wait on it in your test and make sure it has finished? Otherwise loggerMock may not have been called.

Lastly, I recommend adding NSubstitute Analyzers to your test project to pick up any other surprising extension method instances. :)

@simonachmueller
Copy link
Author

Thanks @dtchepak, I switched to TestLogger and it looks like a reasonable alternative to mocking ILogger calls. And thanks for pointing me on NSubstitute Analyzers, it's a great thing!

The await issue was just introduced when I converted my production code to a small sample. Of course I do await on my method in a unit test code.

@dtchepak
Copy link
Member

I thought the await issue may have just been a transcription problem (didn't have async on the method), but thought I should mention it just in case (for anyone else that finds the issue too :) ).

Glad to have helped. I'll close this issue, feel free to re-open it or a new issue if you have other questions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants