Search code examples
c#unit-testingnunitmoqiasyncresult

Mocking a method that uses an asynchronous callback with moq


I'm unit testing some asynchronous code. I have tried to abstract it to make the issue more clear. My issue is that I want to set up the mocked Bar to execute Foo's private callback method after BeginWork returns. The callback is supposed to call Set() on the ManualResetEvent allowing the calling thread to continue to run. When I run the test my thread blocks indefinitely at the call to WaitOne().

Code under test:

using System.Threading;
using NUnit.Framework;
using Moq;
using System.Reflection;

public interface IBar
{
    int BeginWork(AsyncCallback callback);
}

public class Bar : IBar
{
    public int BeginWork(AsyncCallback callback)
    {
        // do stuff
    }   // execute callback
}

public class Foo
{
    public static ManualResetEvent workDone = new ManualResetEvent(false);
    private IBar bar;

    public Foo(IBar bar)
    {
        this.bar = bar;
    }

    public bool DoWork()
    {
        bar.BeginWork(new AsyncCallback(DoWorkCallback));
        workDone.WaitOne(); // thread blocks here
        return true;
    }

    private void DoWorkCallback(int valueFromBeginWork)
    {
        workDone.Set();
    }
}

Test Code:

[Test]
public void Test()
{
    Mock<IBar> mockBar = new Mock<IBar>(MockBehavior.Strict);

    // get private callback
    MethodInfo callback = typeof(Foo).GetMethod("DoWorkCallback", 
                  BindingFlags.Instance | BindingFlags.NonPublic);

    mockBar.Setup(() => BeginWork(It.IsAny<AsyncCallback>())) 
                        .Returns(0).Callback(() => callback.Invoke(0));

    Foo = new Foo(mockBar.Object);
    Assert.That(Foo.DoWork());
}

Solution

  • First observation was that you pass in a mocked ISocket in state and try to cast it to Socket in async callback which will result in a null error which means connectDone.Set() is never called so WaitOne will not unblock.

    Change that to

    private void ConnectCallback(IAsyncResult result) {
        ISocket client = (ISocket)result.AsyncState;
        client.EndConnect(result);
        connectDone.Set();
    }
    

    Second observation was that you were not setting up the mocked calls correctly. No need for reflection here as you needed to get the passed arguments from the mock and invoke then in the mock callback setup

    The following is based on your original code. Review it to get an understanding of what was explained above.

    [TestClass]
    public class SocketManagerTests {
        [TestMethod]
        public void ConnectTest() {
            //Arrange
            var mockSocket = new Mock<ISocket>();
            //async result needed for callback
            IAsyncResult mockedIAsyncResult = Mock.Of<IAsyncResult>();
            //set mock
            mockSocket.Setup(_ => _.BeginConnect(
                    It.IsAny<EndPoint>(), It.IsAny<AsyncCallback>(), It.IsAny<object>())
                )
                .Returns(mockedIAsyncResult)
                .Callback((EndPoint ep, AsyncCallback cb, object state) => {
                    var m = Mock.Get(mockedIAsyncResult);
                    //setup state object on mocked async result
                    m.Setup(_ => _.AsyncState).Returns(state);
                    //invoke provided async callback delegate
                    cb(mockedIAsyncResult);
                });
    
            var manager = new SocketManager(mockSocket.Object);
    
            //Act
            var actual = manager.Connect();
    
            //Assert
            Assert.IsTrue(actual);
            mockSocket.Verify(_ => _.EndConnect(mockedIAsyncResult), Times.Once);
        }
    }
    

    Finally I believe you should consider changing this code to use TPL to get around the whole call back and IAsyncResult drama. Basically exposing an async API and wrapping the calls with a TaskCompletionSource<T> but I guess that is outside of the scope of this question.