测试与外部服务的交互

发布于 2024-12-04 23:24:38 字数 766 浏览 0 评论 0原文

先决条件:我使用的是最新版本的 Play!框架和Java版本(不是Scala)。

我需要在创建用户时将消息发布到消息队列,并且我想测试该行为。我的问题是使其易于测试。

控制器方法

在其他框架中,我会做的是在控制器中使用构造函数注入并在我的测试中传递模拟队列;然而,有了 Play!控制器是静态的,这意味着我无法在测试中执行 new MyController(mockedQueue) 。

我可以使用 Google Guice 并在控制器中的静态字段上添加 @Inject 注释,但这对我来说感觉不太好,因为这意味着我必须将该字段公开才能成为在测试中替换,或者我必须在测试中使用容器。我更喜欢使用构造函数注入,但是玩吧!似乎并没有促进这一点。

模型方法

人们常说你的逻辑应该在你的模型中,而不是你的控制器中。这是有道理的;但是,我们这里不是使用 Ruby,让您的实体与外部服务(电子邮件、消息队列等)交互的可测试性比动态环境中的可测试性要低得多,在动态环境中您只需替换 MessageQueue随意使用模拟实例进行静态调用。

如果我让我的实体呼叫队列,那么如何进行测试?

当然,如果我进行端到端集成测试,这两种情况都是不必要的,但我宁愿不需要消息队列或 SMTP 服务器来运行我的测试。

所以我的问题是:我如何建模我的 Play!控制器和/或模型以促进与外部服务的测试交互?

prerequisites: I'm using the latest version of the Play! framework, and the Java version (not Scala).

I need to publish a message to a message queue when a user is created, and I'd like to test that behaviour. My issue is making this easily testable.

The Controller approach

In other frameworks, what I would've done would be to use constructor injection in the controller and pass in a mocked queue in my tests; however, with Play! the controllers are static, that means I can't do new MyController(mockedQueue) in my tests.

I could use Google Guice and put an @Inject annotation on a static field in my controller, but that doesn't feel very nice to me, as it either means I have to make the field public to be replaced in the test, or I have to use a container in my tests. I'd much prefer to use constructor injection, but Play! doesn't seem to facilitate that.

The Model approach

It's often said your logic should be in your model, not your controller. That makes sense; however, we're not in Ruby here and having your entities interact with external services (email, message queues etc...) is considerably less testable than in a dynamic environment where you could just replace your MessageQueue static calls with a mocked instance at will.

If I make my entity call off to the queue, how is that testable?

Of course, both these situations are unnecessary if I do end-to-end integration tests, but I'd rather not need a message queue or SMTP server spun up for my tests to run.

So my question is: How do I model my Play! controllers and/or models to facilitate testing interactions with external services?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

半岛未凉 2024-12-11 23:24:38

据我所知,对此没有一个干净的解决方案。

您可以为您的依赖项使用抽象工厂。该工厂可以为其生成的对象提供 setter 方法。

public class MyController {
    ...
    private static ServiceFactory serviceFactory = ServiceFactory.getInstance();
    ...
    public static void action() {
        ...
        QueueService queue = serviceFactory.getQueueService();
        ...
    }

您的测试

将如下所示:

public void testAction() {
    QueueService mock = ...
    ...
    ServiceFactory serviceFactory = ServiceFactory.getInstance();
    serviceFactory.setQueueService(mock);
    ...
    MyController.action();
    verify(mock);
}

如果您不想公开工厂的 setter 方法,您可以创建一个接口并在测试中配置实现类。

另一种选择是使用 PowerMock 来模拟静态方法。我以前用过,在大多数情况下效果都很好。只是不要过度使用它,否则您将陷入维护地狱......

最后,既然您愿意在应用程序中使用 Guice,可能是一个可行的选择。

祝你好运!

As I see, there's not a clean solution for this.

You could use an Abstract Factory for your dependencies. This factory could have setter methods for the objects it produces.

public class MyController {
    ...
    private static ServiceFactory serviceFactory = ServiceFactory.getInstance();
    ...
    public static void action() {
        ...
        QueueService queue = serviceFactory.getQueueService();
        ...
    }

}

Your test would look like this:

public void testAction() {
    QueueService mock = ...
    ...
    ServiceFactory serviceFactory = ServiceFactory.getInstance();
    serviceFactory.setQueueService(mock);
    ...
    MyController.action();
    verify(mock);
}

If you don't want to expose the setter methods of the factory, you could create an interface and configure the implementing class in your tests.

Another option would be the use o PowerMock for mocking static methods. I've used before and it works relatively fine for most cases. Just don't overuse it, or you're in maintenance hell...

And finally, since your willing to use Guice in your application, this could be a viable option.

Good luck!

北方的巷 2024-12-11 23:24:38

我有点困惑。您可以调用另一个类的方法。

public class Users extends Controller {
    public static void save(@Valid User user) {
    //check for user validaton
    user = user.save();
    QueueService queueService = new QueueSerice();
    queueService.publishMessage(user);
    }
}

您可以使用模拟为QueueService编写单元测试用例,并为Users控制器save方法编写功能测试用例。

I'm little confused. You can call a method of another class

public class Users extends Controller {
    public static void save(@Valid User user) {
    //check for user validaton
    user = user.save();
    QueueService queueService = new QueueSerice();
    queueService.publishMessage(user);
    }
}

You can write unit testcases for QueueService using a mock and write Functional testcase for Users controller save method.

血之狂魔 2024-12-11 23:24:38

编辑:像以前一样扩展答案并不清楚

第一个想法是将队列的引用添加到模型中,因为您有一个 POJO 并可以访问构造函数。正如您在下面的评论中提到的,在考虑 Hibernate 水合实体时,模型方法是有问题的,这会丢弃它。

第二种方法是将队列的引用添加到控制器。现在看来,这似乎是个坏主意。除了您提到的公共成员问题之外,我相信控制器背后的想法是检索请求的参数,验证它们是否正确(检查真实性,验证等),发送要处理的请求,然后准备响应。

这里的“关键”是“发送要处理的请求”。在某些情况下,如果很简单,我们可以在控制器中完成这项工作,但在其他情况下,似乎最好使用“服务”(以某种方式调用它),在其中您可以使用给定的数据完成所需的工作。

我使用这种分离是因为从测试的角度来看,(对我来说)更容易通过 Selenium 测试控制器并为服务进行单独的测试(使用 JUnit)。

在您的情况下,此服务将包括对您提到的队列的引用。

至于如何初始化,这将取决于。您可以创建一个单例,每次通过构造函数等初始化它。在您的特定场景中,这可能取决于与初始化队列服务相关的工作:如果很难,您可能需要一个具有检索服务的工厂方法的单例(并且可以在测试中进行模拟)并将其作为参数传递给 Service 对象的构造函数。

希望这次更新能更多地澄清我回答时的想法。

EDIT: extending answer as previous was not clear

The first idea would be to add the reference to the Queue to the Model, as you have a POJO and access to the constructor. As you mention in the comments below, the Model approach is problematic when thinking on Hibernate hydrating the entity, which would discard this.

The second approach would be to add this reference to the Queue to the Controller. Now, this seems like a bad idea. Besides the public member issue you mention, I believe that the idea behind the controller is to retrieve the parameters to the request, validate they are correct (checkAuthenticity, validation, etc), send the request to be processed and then prepare the response.

The "key" here is "send the request to be processed". In some cases we may do that work in the Controller if it is simple, but in other cases it seems better to use a "Service" (to call it somehow) in which you do the work you need with the given data.

I use this separation as from the point of view of testing it's easier (for me) to test the controller via Selenium and do a separate test (using JUnit) for the service.

In you case this Service would include the reference to the Queue you mention.

On how to initialize, that will depend. You may create a singleton, initialize it every time via a constructor, etc. In you particular scenario this may depend on the work related to initialize your queue service: if it's hard you may want a Singleton with a Factory method that retrieves the service (and can be mocked in testing) and pass that as a parameter to the constructor of the Service object.

Hope this update clarifies more what I had in mind when I answered.

梦里寻她 2024-12-11 23:24:38

这可能不是您想要的,但在我当前的项目中,我们已经通过集成测试以及带有本地队列和消息传递桥的 JMS 设置解决了此类测试。

更详细地说:

  • 您的代码始终向本地队列发布消息/从本地队列读取消息,即本地应用程序服务器上的队列(而不是外部系统上的队列)。
  • 消息传递桥在需要时将本地队列连接到外部服务的队列,例如在生产或手动测试环境中。
  • 集成测试创建新用户(或您想要测试的任何内容),然后从本地队列读取预期消息。在这种情况下,消息传递桥不处于活动状态。

在我的项目中,我们使用 SoapUI 来执行这些测试,因为被测系统是一个基于 SOAP 的集成平台,并且SoapUI 具有良好的 JMS 支持。但它也可以是一个普通的 JUnit 测试,该测试执行测试并随后从本地 JMS 队列读取。

It is perhaps not what you are looking for, but in my current project we have solved that type of testing through integration tests and a JMS setup with a local queue and a messaging bridge.

In slightly more detail:

  • Your code always posts/reads messages to/from local queues, i.e. queues on your local app server (not on the external system).
  • A messaging bridge connects the local queue to the queue of the external service when needed, e.g. in production or in a manual testing environment.
  • An integration test creates the new user (or whatever you want to test), and then reads the expected message from the local queue. In this case, the messaging bridge is not active.

On my project, we use SoapUI to perform these tests as the system under test is a SOAP-based integration platform and SoapUI has good JMS support. But it could just as well be a plain JUnit test which performs the test and reads from the local JMS queue afterwards.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文