【问题标题】:How to test Java app operating directly on external API如何测试直接在外部 API 上运行的 Java 应用程序
【发布时间】:2010-11-21 13:44:22
【问题描述】:

从 Ruby 世界回来后,我在 Java 中进行 TDD 时遇到了一些问题。最大的问题是当我的应用程序只是与外部 API 通信时。

假设我只想从 Google 日历中获取一些数据,或者从某个 Twitter 用户那里获取 5 条推文并显示出来。

在 Ruby 中,我没有任何问题,因为我可以直接在测试中对 API 库进行猴子补丁,但在 Java 中我没有这样的选择。

如果我从 MVC 的角度考虑这一点,我的模型对象是通过某个库直接访问 API。问题是,这是糟糕的设计吗?我是否应该始终将任何 API 库包装在某个接口中,以便可以在 Java 中模拟/存根它?

因为当我想到这一点时,该界面的唯一目的是模拟(请不要因为这样说而杀了我)猴子补丁。这意味着每当我使用任何外部资源时,我都必须将每一层包装在可以被存根的接口中。

# do I have to abstract everything just to do this in Java?
Twitter.stub!(:search)

现在你可能会说我应该总是抽象出接口,这样我就可以将底层更改为其他任何东西。但如果我在写 twitter 应用程序,我不会将其更改为 RSS 阅读器。

是的,我可以添加例如 Facebook,然后有界面是有意义的。但是当没有其他资源可以替代我正在使用的资源时,我仍然必须将所有内容包装在接口中以使其可测试。

我是否遗漏了什么,或者这只是在 Java 世界中进行测试的一种方式?

【问题讨论】:

    标签: java ruby testing junit rspec


    【解决方案1】:

    在 Java 中使用接口通常是一种很好的做法。有些语言有多重继承,有些语言有鸭子类型,Java 有接口。这是该语言的一个关键特性,它让我可以使用

    1. 一个类在不同上下文中的不同方面和
    2. 同一合约的不同实现,无需更改客户端代码。

    所以接口是一个您应该普遍接受的概念,然后您将在这种情况下获得好处,您可以用模拟对象替换您的服务。

    关于 Java 最佳实践的最重要的书籍之一是 Effective Java by Joshua Bloch。我强烈建议您阅读它。在这种情况下,最重要的部分是第 52 条:通过接口引用对象。引用:

    更一般地说,您应该更倾向于使用接口而不是 类来引用对象。 如果存在适当的接口类型,则参数、返回值、变量和字段都应使用接口声明 类型。你真正需要引用一个对象的类的唯一时间是当你 使用构造函数创建它。

    如果你更进一步(例如,当使用依赖注入时),你甚至不会调用构造函数。

    转换语言的一个关键问题是你也必须转换思维方式。在用语言 y 思考时,您无法有效地编程语言 x。不使用指针就无法有效地编写 C,Ruby 不能不使用鸭子类型,Java 不能不使用接口。

    【讨论】:

    • 当然(抱歉耽搁了)!我不认为 Effective Java 中的“第 52 项”说开发人员应该为每个类创建单独的 Java 接口,正如您所暗示的那样。请注意“如果存在适当的接口类型......”的部分。当它们存在时,使用隐式类接口就很好了。不应滥用单独的接口。
    • 不,不是针对每个类(显然不是针对实体和其他价值持有者,也不是针对帮助类),但我确实建议所有服务类都应该由接口支持(因为你不想要在服务中实现紧密耦合,并且您希望能够传入模拟或代理,坦率地说:因为这就是接口的用途)。
    • 通常情况下,我不会让我的服务类实现单独的接口,仅仅是因为所述接口往往永远不会获得第二个实现。 (我不计算“模拟”实现,因为那只是 一个 用于模拟的特定实现策略——而且是一个糟糕的策略。)对我来说,单独的接口主要是为了支持替代实现的可插入性(即生产代码中的真实代码)用于稳定的抽象。
    【解决方案2】:

    包装外部 API 是我这样做的方式。

    所以,正如您已经说过的,您将拥有一个接口和两个类:真实的和虚拟的实现。

    是的,从某些特定服务的角度来看,这似乎不合理,例如 Twitter。但是,这样您的构建过程不依赖于外部资源。依赖外部库并不是那么糟糕,但是让您的测试依赖于网络上存在或不存在的实际数据可能会扰乱构建过程。

    最简单的方法是使用您的接口/类对包装 API 服务,并在整个代码中使用它。

    【讨论】:

    • 但是当它没有增加任何价值时,为什么还要麻烦创建一个包装器呢?就像在 Ruby 中一样,在 Java 中,我们也可以在编写单元测试时为 any 类提供模拟/存根实现。
    • 我绝对不会说它没有“增加价值”,因为它完全符合需要。现在,您指出的另一个问题是额外的努力,但是您忽略了这样一个事实,即我的答案可以从纯 Java 中获得,没有任何额外的依赖项。为什么我要付出额外的努力来获得额外的依赖项并担心它的兼容性、许可证......如果我能简单地做到这一点。
    • 你会包装,比如说,Apache Commons Email API吗?它已经封装了更复杂的 Java Mail API 并且非常易于使用(只需实例化 SimpleEmail,设置一些属性,然后调用 send)。显然,在此之上的另一层间接没有额外的价值。关于使用模拟库的额外努力,如果它节省的成本超过成本,为什么不呢? (我可以保证,对于任何实际数量的单元测试来说,手工编写模拟/存根所付出的额外努力要大得多。)
    • 哦,是的.. 包装对测试没有用处。它还减少了耦合,这对于外部服务尤为重要。它是通过其余代码使用的包装器。因此,如果(阅读:何时)服务提供者更改了某些内容,那么您必须做出的唯一差异希望只能应用于包装器。到目前为止,依赖外部服务是我见过的最糟糕的耦合。
    • 您确实在这里提出了观点!但是,请注意 Apache Commons Email API 只是一个库,而 Twitter API 或 Adwords API 也是外部服务。我肯定会把它们包起来。
    【解决方案3】:

    我知道你想要的是Mock objects

    正如您所描述的,生成对象“测试版本”的方法之一是实现一个通用接口并使用它。

    但是,您缺少的是简单地扩展类(前提是它未声明final)并覆盖您要模拟的方法。 (注意:这样做的可能性是库将其类声明为 final 被认为是错误形式的原因——它会使测试变得相当困难。)

    有许多 Java 库旨在促进 Mock 对象的使用 - 您可以查看 MockitoEasyMock

    【讨论】:

    • 是的,我可以扩展它,但这需要有另一个层将“API”对象(测试中的模拟版本)传递给模型。但我想这是没有办法的。
    • 好吧,在这种情况下,您可以使用工厂方法创建对象,也可以使用反射替换模型中的私有字段。
    【解决方案4】:

    Mockito 更方便,就像你的 ruby​​ 模拟。

    【讨论】:

      【解决方案5】:

      可以用Java“猴子补丁”一个API。 Java 语言本身并没有提供具体的方法来做到这一点,但 JVM 和标准库提供。在 Ruby 中,开发人员可以为此使用 Mocha 库。在 Java 中,您可以使用 JMockit 库(我创建该库是因为旧版模拟工具的限制)。

      这是一个示例 JMockit 测试,相当于 Mocha documentation 中的 test_should_calculate_value_of_unshipped_orders 测试:

         @Test
         public void shouldCalculateValueOfUnshippedOrders()
         {
            final Order anOrder = new Order();
            final List<Order> orders = asList(anOrder, new Order(), new Order());
      
            new NonStrictExpectations(Order.class)
            {{
               Order.findAll(); result = orders;
               anOrder.getTotalCost(); result = 10;
            }};
      
            assertEquals(30, Order.unshippedValue());
         }
      

      【讨论】:

        猜你喜欢
        • 2013-04-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-04-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多