【发布时间】:2015-02-26 01:07:29
【问题描述】:
我们将 MassTransit 异步消息传递(在 RabbitMQ 之上)用于我们的微服务架构。
我们在测试消费者反过来进行异步调用时遇到了问题。
下面的示例显示了一个简单的 MassTransit 消费者,它使用 RestSharp 进行出站调用并利用 ExecuteAsync 异步方法。
public class VerifyPhoneNumberConsumer : Consumes<VerifyPhoneNumber>.Context
{
IRestClient _restClient;
RestRequest _request;
PhoneNumber _phoneNumber;
PhoneNumberVerificationResponse _responseData;
public VerifyPhoneNumberConsumer(IRestClient client)
{
_restClient = client;
}
public void Consume(IConsumeContext<VerifyPhoneNumber> context)
{
try
{
//we can do some standard message verification/validation here
_restClient.ExecuteAsync<PhoneNumberVerificationResponse>(_request, (response) =>
{
//here we might do some standard response verification
_responseData = response.Data;
_phoneNumber = new PhoneNumber()
{
Number = _responseData.PhoneNumber
};
context.Respond(new VerifyPhoneNumberSucceeded(context.Message)
{
PhoneNumber = _phoneNumber
});
});
}
catch (Exception exception)
{
context.Respond(new VerifyPhoneNumberFailed(context.Message)
{
PhoneNumber = context.Message.PhoneNumber,
Message = exception.Message
});
}
}
}
对此的示例单元测试可能如下所示:
[TestFixture]
public class VerifyPhoneNumberConsumerTests
{
private VerifyPhoneNumberConsumer _consumer;
private PhoneNumber _phoneNumber;
private RestResponse _response;
private VerifyPhoneNumber _command;
private AutoResetEvent _continuationEvent;
private const int CONTINUE_WAIT_TIME = 1000;
[SetUp]
public void Initialize()
{
_continuationEvent = new AutoResetEvent(false);
_mockRestClient = new Mock<IRestClient>();
_consumer = new VerifyPhoneNumberConsumer(_mockRestClient.Object);
_response = new RestResponse();
_response.Content = "Response Test Content";
_phoneNumber = new PhoneNumber()
{
Number = "123456789"
};
_command = new VerifyPhoneNumber(_phoneNumber);
}
[Test]
public void VerifyPhoneNumber_Succeeded()
{
var test = TestFactory.ForConsumer<VerifyPhoneNumberConsumer>().New(x =>
{
x.ConstructUsing(() => _consumer);
x.Send(_command, (scenario, context) => context.SendResponseTo(scenario.Bus));
});
_mockRestClient.Setup(
c =>
c.ExecuteAsync(Moq.It.IsAny<IRestRequest>(),
Moq.It
.IsAny<Action<IRestResponse<PhoneNumberVerificationResponse>, RestRequestAsyncHandle>>()))
.Callback<IRestRequest, Action<IRestResponse<PhoneNumberVerificationResponse>, RestRequestAsyncHandle>>((
request, callback) =>
{
var responseMock = new Mock<IRestResponse<PhoneNumberVerificationResponse>>();
responseMock.Setup(r => r.Data).Returns(GetSuccessfulVericationResponse());
callback(responseMock.Object, null);
_continuationEvent.Set();
});
test.Execute();
_continuationEvent.WaitOne(CONTINUE_WAIT_TIME);
Assert.IsTrue(test.Sent.Any<VerifyPhoneNumberSucceeded>());
}
private PhoneNumberVerificationResponse GetSuccessfulVericationResponse()
{
return new PhoneNumberVerificationResponse
{
PhoneNumber = _phoneNumber
};
}
}
由于在消费者中调用了 ExecuteAsync 方法,如果我们在发出信号(或超时)之前不放置一些东西来阻止它,这个测试方法就会失败。在上面的示例中,我们使用 AutoResetEvent 从回调发出信号以继续并运行断言。
这是一种糟糕的方法,我们正在用尽所有资源来寻找替代方法。如果它不明显,这可能会在测试期间导致错误的失败和竞争条件。更不用说可能导致严重的自动化测试时间了。
我们有哪些替代品比我们目前拥有的更好。
编辑这是我最初用于如何模拟 RestSharp 异步调用的源代码。
【问题讨论】:
-
你需要有一个设计用来支持异步方法的测试框架;一种正确测试返回
Task的方法,设置适当的消息循环,通过返回的Task观察故障等。如果您的框架不支持它,您或多或少需要做所有那个给自己下水道。如果支持,请研究您选择的测试框架如何支持它。 -
旁注:我在您的测试中没有看到任何异步代码 - 看起来
ExecuteAsync的模拟版本是完全同步的,我不确定您为什么需要所有这些事件舞蹈...将回调延迟到方法返回后可能会更好(保存回调并在Consumer返回后手动调用它)... -
Moq 回调中的代码是在
text.Execute()之后异步运行的,如果我不将reset 事件放在回调代码中,则代码失败并且测试通过。此外,消费者不会返回,因为他们正在消息总线上响应。您所看到的“事件舞蹈”实际上只是异步消息处理。 -
@TheJediCowboy 如果您使用 TPL 以外的异步模型处理异步代码,您可能需要通过包装方法将这些操作转换为 TPL,除非有测试框架可以支持测试异步方法的回调样式(或其他样式)。
-
NUnit 支持异步单元测试(2.6.4 无论如何)。测试异步已经够难了。
标签: c# asynchronous async-await restsharp masstransit