4

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());
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
Matt C.
  • 51
  • 4
  • Shouldn't you capture the call back in the return with something like this `.Returns((ep, cb, st) => { callback = cb; return mockedResult; });` – juharr Jun 28 '19 at 15:09
  • Why cast back to `Socket` here `Socket client = (Socket)result.AsyncState;` and not to the abstraction `ISocket` – Nkosi Jun 28 '19 at 15:15
  • BeginConnect() has a return type of IAsyncResult. Passing anything else to Returns() causes an error. – Matt C. Jun 28 '19 at 15:17
  • 1
    Have you considered changing this code to use TPL to get around the whole call back IAsyncResult drama. Basically exposing an async API and wrapping the calls with a `TaskCompletionSource` – Nkosi Jun 28 '19 at 15:20
  • Casting to socket doesn't really matter for this issue. It is an artifact of code written by coworkers that I haven't modified yet to be 100% testable. – Matt C. Jun 28 '19 at 15:21
  • 1
    I would like to test this but the currently provided code will not compile. Provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) that can be used to reproduce the problem, allowing a better understanding of what is being asked. – Nkosi Jun 28 '19 at 15:25
  • @MattC. The cast is important because you pass in a mocked `ISocket` in state and try to cast it to `Socket` in callback which will result in a null error which means `connectDone.Set()` is never called so `WaitOne` will block. – Nkosi Jun 28 '19 at 15:58
  • @Nsoki I casted to ISocket and the thread still blocks. I tried to abstract the code to make the issue clearer. I don't think it will compile though because I don't know how to invoke the AsyncCallback properly. Sorry I'm a bit of a noob. I wrote my first line of c# this week. – Matt C. Jun 28 '19 at 16:19
  • @MattC. no worries. I am testing a solution now. You are setting up the mock incorrectly. – Nkosi Jun 28 '19 at 16:21
  • @Nsoki I think you're right. I don't think I am setting up the callback correctly. Thank you so much for the help :) – Matt C. Jun 28 '19 at 16:23
  • @MattC. I added an answer that shows how to solve the problem as originally state. Had to make some guesses to fill in the blanks but I believe this should work for you. If it resolves your problem remember to mark it as the accepted answer. – Nkosi Jun 28 '19 at 16:46
  • @MattC. and welcome to Stackoverflow, when you get some time review [ask] a question so you can get better responses in future – Nkosi Jun 28 '19 at 16:51

1 Answers1

0

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.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • I got it to work! There was a call to EndConnect in the callback that required a setup. Thank you so much for the help this had been driving me nuts for a couple days! I will update the question after work to make everything clear. – Matt C. Jun 28 '19 at 17:59
  • @MattC. yeah you would have seen that I removed the `MockBehavior.Strict` that would have cause that error. – Nkosi Jun 28 '19 at 18:00
  • @MattC. glad you eventually got it to work. Happy Coding. – Nkosi Jun 28 '19 at 18:03
  • It blocked when I pasted your code directly (with a couple minor adjustments) even though the mocks were loose. Not sure how to explain that, but it doesn't matter, it works now. – Matt C. Jun 28 '19 at 18:08
  • @MattC. Saw your comment earlier and was not in a position to reply as I was offsite. Much appreciated. – Nkosi Jun 29 '19 at 00:17