简体   繁体   中英

Is it possible to call an async callback with Moq when a mocked method is called?

I'm mocking some implementation using Moq and I want verify a method is called correctly on this interface, the problem is it looks something like this:

public interface IToBeMocked {
    void DoThing(IParameter parameter);
}

public interface IParameter {
    Task<string> Content { get; }
}

So I set up my mock:

var parameter = "ABC";
var mock = new Mock<IToBeMocked>();
mock
    .Setup(m => m.DoThing(It.IsAny<IParameter>()))
    .Callback<IParameter>(p async => (await p.Content).Should().Be(parameter));

new Processor(mock.Object).Process(parameter);

mock
    .Verify(m => m.DoThing(It.IsAny<IParameter>()), Times.Once);

Unfortunately, this test already passes with the following implementation:

public class Processor {
    public Processor(IToBeMocked toBeMocked){
        _toBeMocked = toBeMocked;
    }

    public void Process(string parameter){
        _toBeMocked.DoThing(null);
    }
}

Because the Callback is async but the signature requires an Action, meaning the awaiter is never awaited, and the test ends before the exception is thrown.

Is there any functionality in Moq to await an asynchronous callback?

Edit

There seems to be some confusion. I hope this clarifies the problem.

I'm doing TDD. I've implemented the simplest shell of the code to make the test compile. Then I have written the test to ensure "ABC" is the result of the Task and I've set the test running. It's passing. This is the issue . I want the test to fail, so I can implement the 'real' implementation.

Edit 2

The more I think about this the more I think this is probably impossible. I've opened an issue for the repo: https://github.com/moq/moq4/issues/737 but I was thinking about the implementation of such a feature as I was writing the request in anticipation of submitting a PR, and it seems impossible. Still if anyone has any ideas I'd love to hear them either here or in the GitHub issue and I'll keep both places up to date. For now, I suppose I will have to use a stub.

The call back expects an Action, you attempt to perform an async operation in said callback which boils down to async void call. Exceptions cannot be caught in such situations as they are fire and forget.

Reference Async/Await - Best Practices in Asynchronous Programming

So the problem is not with the Moq framework but rather the approach taken.

Use the call back to get the desired parameter and work from there.

Review the progression of the following tests to see how the TDD approach evolves with each test.

[TestClass]
public class MyTestClass {
    [Test]
    public void _DoThing_Should_Be_Invoked() {
        //Arrange            
        var parameter = "ABC";
        var mock = new Mock<IToBeMocked>();
        mock
            .Setup(m => m.DoThing(It.IsAny<IParameter>()));

        //Act
        new Processor(mock.Object).Process(parameter);

        //Assert            
        mock.Verify(m => m.DoThing(It.IsAny<IParameter>()), Times.Once);
    }

    [Test]
    public void _Parameter_Should_Not_Be_Null() {
        //Arrange
        IParameter actual = null;

        var parameter = "ABC";
        var mock = new Mock<IToBeMocked>();
        mock
            .Setup(m => m.DoThing(It.IsAny<IParameter>()))
            .Callback<IParameter>(p => actual = p);

        //Act
        new Processor(mock.Object).Process(parameter);

        //Assert
        actual.Should().NotBeNull();
        mock.Verify(m => m.DoThing(It.IsAny<IParameter>()), Times.Once);
    }

    [Test]
    public async Task _Parameter_Content_Should_Be_Expected() {
        //Arrange

        IParameter parameter = null;

        var expected = "ABC";
        var mock = new Mock<IToBeMocked>();
        mock
            .Setup(m => m.DoThing(It.IsAny<IParameter>()))
            .Callback<IParameter>(p => parameter = p);

        new Processor(mock.Object).Process(expected);

        parameter.Should().NotBeNull();

        //Act
        var actual = await parameter.Content;

        //Assert
        actual.Should().Be(expected);
        mock.Verify(m => m.DoThing(It.IsAny<IParameter>()), Times.Once);
    }
}

It MAY or MAY NOT actually work if you use an async method signature on the callback. Depends if your runtime decides to continue with the same thread or spin up a new one. If it makes sense, and it often does, to do some Asserts or grab some parameters for later validation in the Callback, you should remove the async stuff all together and force it to run via

Task.Run(() => AsyncMethodHere).Result

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM