模拟使用最小起订量的异步回调的方法
Mocking a method that uses an asynchronous callback with moq
我正在对一些异步代码进行单元测试。我试图将其抽象化以使问题更清楚。我的问题是我想设置模拟 Bar
以在 BeginWork
returns 之后执行 Foo
的私有回调方法。回调应该在 ManualResetEvent
上调用 Set()
,允许调用线程继续到 运行。当我 运行 测试时,我的线程在调用 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());
}
首先观察到您在 state
中传递了一个模拟的 ISocket
并尝试在异步回调中将其转换为 Socket
这将导致空错误,这意味着 connectDone.Set()
永远不会被调用,所以 WaitOne
不会解锁。
将其更改为
private void ConnectCallback(IAsyncResult result) {
ISocket client = (ISocket)result.AsyncState;
client.EndConnect(result);
connectDone.Set();
}
第二个观察是您没有正确设置模拟调用。这里不需要反射,因为您需要从模拟中获取传递的参数,然后在模拟回调设置中调用
以下内容基于您的原始代码。查看它以了解上面解释的内容。
[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>
包装调用,但我想这超出了这个问题的范围。
我正在对一些异步代码进行单元测试。我试图将其抽象化以使问题更清楚。我的问题是我想设置模拟 Bar
以在 BeginWork
returns 之后执行 Foo
的私有回调方法。回调应该在 ManualResetEvent
上调用 Set()
,允许调用线程继续到 运行。当我 运行 测试时,我的线程在调用 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());
}
首先观察到您在 state
中传递了一个模拟的 ISocket
并尝试在异步回调中将其转换为 Socket
这将导致空错误,这意味着 connectDone.Set()
永远不会被调用,所以 WaitOne
不会解锁。
将其更改为
private void ConnectCallback(IAsyncResult result) {
ISocket client = (ISocket)result.AsyncState;
client.EndConnect(result);
connectDone.Set();
}
第二个观察是您没有正确设置模拟调用。这里不需要反射,因为您需要从模拟中获取传递的参数,然后在模拟回调设置中调用
以下内容基于您的原始代码。查看它以了解上面解释的内容。
[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>
包装调用,但我想这超出了这个问题的范围。