与外部服务交互的测试

8

前提条件:我正在使用最新版本的Play!框架和Java版本(而不是Scala)。

当创建用户时,我需要将消息发布到消息队列,并且我希望测试这种行为。我的问题在于如何使其易于测试。

控制器方法

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

我可以使用Google Guice并在我的控制器中的静态字段上放置一个@Inject注释,但是这对我来说并不好,因为它要么意味着我必须将该字段公开以便在测试中替换它,要么我必须在我的测试中使用容器。我更喜欢使用构造函数注入,但是Play!似乎没有提供这个功能。

模型方法

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

如果我让我的实体调用队列,那怎么测试呢?

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

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

4个回答

3

我看到,这个问题没有一个干净的解决方案。

你可以使用抽象工厂来处理你的依赖关系。这个工厂可以为它生产的对象设置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,this可能是一个可行的选择。

祝你好运!


ServiceFactory 方法的一个 潜在 问题是字段何时被初始化;由于它是静态字段,因此在您有机会替换实例之前,初始化程序可能会通过 getInstance 调用。只是随便想想,没有实际验证过这一点。 - James Gregory
实际上,在这个例子中,ServiceFactory 是一个 Singleton。你不会替换 ServiceFactory 本身,而是它“生产”的 QueueService。 - Andre Rodrigues
我明白这一点;然而,由于该字段是静态的,存在可能在你替换getInstance返回的任何值之前就已经被初始化的情况。通常你无法控制静态初始化器的执行时间,因此它可能在你的测试设置发生之前就已经被执行了。 - James Gregory
好的,我理解您的关注,但我们实际上并没有嘲笑ServiceFactory。无论我们在测试代码还是生产代码中调用getInstance()方法,它都将始终返回相同的单例实例(即ServiceFactory实例)。我们正在配置ServiceFactory,以便我们可以替换从getQueueService()方法返回的值。 - Andre Rodrigues
我现在明白了,谢谢。ServiceFactory 有效地返回单例似乎有点奇怪,但我可以理解这样做的理由。谢谢,是我的错误。 - James Gregory
嗯,ServiceFactory 并不需要为其生产的每个服务返回单例实例。只是在这个例子中,ServiceFactory 本身是一个单例。getInstance() 方法提供了访问 ServiceFactory 实例的方式。 - Andre Rodrigues

2

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

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方法的功能测试用例。

也许我没有表达清楚。在你的测试中,你会如何用模拟实例替换 new QueueService()?或者你是说你会对 QueueService 进行单元测试,但只对 UserQueueService 之间的交互进行功能测试? - James Gregory
如果这是正确的方法,那很好;但在我处理过的大多数其他框架中,完全可以在不需要实际存在的队列的情况下测试两者之间的交互。 - James Gregory

2

编辑:之前的回答不够清晰,现在进行扩展

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

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

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

我将此分离,因为从测试的角度来看,通过Selenium测试控制器更容易(对我来说),并且可以通过JUnit对服务进行单独测试。

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

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

希望此更新更清楚地说明了我回答时的想法。


鸡和蛋的问题。那么工厂从哪里来?一个单例模式? - James Gregory
鸡和蛋?:| 你可以在单例、工具类或其他地方编写一个静态工厂方法,其唯一目的是返回所需的队列对象。我的意思是,你还有什么其他方式可以做到呢?恐怕我没有看到问题或者我不理解你的反对意见。 - Pere Villega
在任何其他框架中,我都会使用构造函数注入(带或不带容器)。使用单例或工厂方法只会隐藏一个类真正具有的依赖关系。 - James Gregory
但是 Model 有构造函数... 那就是你想放它的地方... 而实体是一个POJO,绝对可测试... - Pere Villega
除非它是由Hibernate填充的模型。 - James Gregory

1

也许这不是你想要的,但在我的当前项目中,我们通过集成测试和使用本地队列和消息桥接来解决了这种类型的测试。

稍微详细一些:

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接