【问题标题】:Unit testing MassTransit consumers that make utilize asynchronous calls对利用异步调用的 MassTransit 消费者进行单元测试
【发布时间】: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 异步调用的源代码。

How to test/mock RestSharp ExecuteAsync(...)

【问题讨论】:

  • 你需要有一个设计用来支持异步方法的测试框架;一种正确测试返回Task的方法,设置适当的消息循环,通过返回的Task观察故障等。如果您的框架不支持它,您或多或少需要做所有那个给自己下水道。如果支持,请研究您选择的测试框架如何支持它。
  • 旁注:我在您的测试中没有看到任何异步代码 - 看起来 ExecuteAsync 的模拟版本是完全同步的,我不确定您为什么需要所有这些事件舞蹈...将回调延迟到方法返回后可能会更好(保存回调并在Consumer 返回后手动调用它)...
  • Moq 回调中的代码是在text.Execute() 之后异步运行的,如果我不将reset 事件放在回调代码中,则代码失败并且测试通过。此外,消费者不会返回,因为他们正在消息总线上响应。您所看到的“事件舞蹈”实际上只是异步消息处理。
  • @TheJediCowboy 如果您使用 TPL 以外的异步模型处理异步代码,您可能需要通过包装方法将这些操作转换为 TPL,除非有测试框架可以支持测试异步方法的回调样式(或其他样式)。
  • NUnit 支持异步单元测试(2.6.4 无论如何)。测试异步已经够难了。

标签: c# asynchronous async-await restsharp masstransit


【解决方案1】:

老实说,执行异步方法的复杂性是 MassTransit 3 的主要驱动因素之一。虽然它还没有准备好,但它使消费者的异步方法调用变得更好。

您在上面测试的内容,因为您在 REST 客户端上调用 ExecuteAsync(),而不是在消费者中等待响应(使用 .Result 或 .Wait),所以 HTTP 调用在消息消费者之后继续已经回来了。所以这可能是你的问题的一部分。

在 MT3 中,这个消费者会写成:

public async Task Consume(ConsumeContext<VerifyPhoneNumber> context)
{
    try
    {
        var response = await _restClient
            .ExecuteAsync<PhoneNumberVerificationResponse>(_request);
        var phoneNumber = new PhoneNumber()
        {
            Number = response.PhoneNumber
        };

        await context.RespondAsync(new VerifyPhoneNumberSucceeded(context.Message)
        {
            PhoneNumber = _phoneNumber
        });
    }
    catch (Exception exception)
    {
        context.Respond(new VerifyPhoneNumberFailed(context.Message)
        {
            PhoneNumber = context.Message.PhoneNumber,
            Message = exception.Message
        });
    }        
}

【讨论】:

  • 直到我发布了我的之后才看到这个。这几乎正​​是我想出的。除了我需要回到我们与 MassTransit 一起扩展的内容,以便考虑等待context.RespondAsync(...)。我将以你的为例。
【解决方案2】:

我能够提出以下解决方案,它看起来更加优雅和适当。如果我的假设有误,请随时纠正我。

我修改了使用者中的 RestSharp 执行,因此我的使用者如下所示:

公共类 VerifyPhoneNumberConsumer : Consumes.Context { IRestClient _restClient; 休息请求_请求; 电话号码_电话号码; PhoneNumberVerificationResponse _responseData;

public VerifyPhoneNumberConsumer(IRestClient client)
{
    _restClient = client;
}

public void Consume(IConsumeContext<VerifyPhoneNumber> context)
{
    try
    {
        //we can do some standard message verification/validation here 

        var response = await _restClient.ExecuteGetTaskAsync<PhoneNumberVerificationResponse>(_request);

        _responseData = response.Data;

        _phoneNumber = new PhoneNumber()
        {
            Number = _responseData.PhoneNumber
        };
    }
    catch (Exception exception)
    {
        context.Respond(new VerifyPhoneNumberFailed(context.Message)
        {
            PhoneNumber = context.Message.PhoneNumber,
            Message = exception.Message
        });
    }
}

}

这利用了 RestSharp 的 TPL 异步功能,因此我不必自己做。

因此,我可以将我的测试代码更改为以下内容:

[Test]
public void VerifyPhoneNumber_Succeeded()
{
    var test = TestFactory.ForConsumer<VerifyPhoneNumberConsumer>().New(x =>
    {
        x.ConstructUsing(() => _consumer);
        x.Send(_command, (scenario, context) => context.SendResponseTo(scenario.Bus));
    });

    var response = (IRestResponse<PhoneNumberVerificationResponse>)new RestResponse<PhoneNumberVerificationResponse>();
    response.Data = GetSuccessfulVericationResponse();

    var taskResponse = Task.FromResult(response);
    Expect.MethodCall(
        () => _client.ExecuteGetTaskAsync<PhoneNumberVerificationResponse>(Any<IRestRequest>.Value.AsInterface))
          .Returns(taskResponse);

    test.Execute();

    Assert.IsTrue(test.Sent.Any<VerifyPhoneNumberSucceeded>());
}

【讨论】:

    猜你喜欢
    • 2019-09-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-07-07
    • 1970-01-01
    • 2022-01-15
    相关资源
    最近更新 更多