Насмешка над методом, который использует асинхронный обратный вызов с moq - PullRequest
3 голосов
/ 28 июня 2019

Я занимаюсь модульным тестированием некоторого асинхронного кода. Я попытался абстрагироваться, чтобы сделать проблему более ясной. Моя проблема в том, что я хочу настроить поддельный Bar для выполнения приватного метода обратного вызова Foo после возврата BeginWork. Обратный вызов должен вызывать Set() на ManualResetEvent, позволяя вызывающему потоку продолжать работу. Когда я запускаю тест, мой поток блокируется на неопределенный срок при вызове WaitOne().

Проверяемый код:

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]
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());
}

1 Ответ

0 голосов
/ 28 июня 2019

Первое наблюдение состояло в том, что вы передаете поддельное ISocket в state и попытаетесь привести его к Socket в асинхронном обратном вызове, что приведет к нулевой ошибке, что означает, что connectDone.Set() никогда не вызывается, поэтому WaitOneне разблокирует.

Измените это на

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

Вторым наблюдением было то, что вы неправильно настраивали ложные вызовы.Здесь не нужно размышлять, поскольку вам нужно было получить переданные аргументы из mock и затем вызвать их в настройке обратного вызова mock

Следующее основано на вашем исходном коде.Просмотрите его, чтобы понять, что было объяснено выше.

[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);
    }
}

Наконец, я считаю, что вам следует рассмотреть возможность изменения этого кода, чтобы использовать TPL, чтобы обойти весь обратный вызов и драму IAsyncResult.По сути, раскрытие асинхронного API и упаковка вызовов TaskCompletionSource<T>, но я думаю, что это выходит за рамки этого вопроса.

...