17

大约半年前,我的组织开始使用 Pact 来创建/验证用 Java 编写的 REST 服务/微服务之间的合同。我们很难决定提供商测试的适当范围或掌握程度,并且希望从其他协议用户的经验中获得一些输入。

基本上,讨论围绕在提供者测试中模拟/存根的位置展开。在服务中,您至少必须模拟对其他服务的外部调用,但您也可以选择模拟更接近 REST 资源类。

我们将其归结为两个选项:

1.第一个选项是提供者测试应该是一个严格的契约测试,并且只使用提供者服务的 REST 资源类,模拟/存根从那里使用的服务类/编排器等。该合同测试将通过组件测试进行扩充,这些组件测试将测试由供应商测试存根/模拟的部分。

2.第二种选择是使用提供者测试作为组件测试,为每个请求执行整个服务组件。只有对其他组件的传递外部调用才会被模拟/存根。

这些是专业人士对每个选项的想法

选项 1 的专业人士:

  • 测试将更易于实施,并且占用空间更小
    => 更高程度的隔离。
  • 无论如何,我们可能需要其他组件测试来涵盖通常未包含在消费者期望中的用例(错误流等)。这样我们就不会在一个包中混合不同类型的组件测试(Pact 和其他),从而使测试套件更易于理解。

选项 2 的专业人士:

  • 测试正在执行更多“真实”代码=>由于不良模拟/存根导致测试错误的风险降低。

我真的很想听听您的提供商测试在这方面的典型表现。有最佳实践吗?

澄清我们所说的“组件”是什么意思:一个组件是一个微服务或一个更大的服务应用程序中的一个模块。我们从 Martin Fowlers http://martinfowler.com/articles/microservice-testing/中获取了“组件”的定义。

提供者服务/组件通常在 Jersey 资源类中具有 REST 端点。此端点是 Pact 提供者测试的提供者端点。一个例子:

@Path("/customer")
public class CustomerResource {

    @Autowired private CustomerOrchestrator customerOrchestrator;

    @GET
    @Path("/{customerId}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response get(@PathParam("customerId") String id) {
        CustomerId customerId = CustomerIdValidator.validate(id);
        return Response.ok(toJson(customerOrchestrator.getCustomer(customerId))).build();
    }

在上面的示例中,@Autowired(我们使用 spring)CustomerOrchestrator 可以在运行提供程序测试时被模拟,或者您可以注入真正的“Impl”类。如果您选择注入真正的“CustomerOrchestratorImpl.class”,它将具有额外的 @Autowired bean 依赖项,而这些依赖项又可能具有其他...一个 REST 客户端,它将对其他下游服务/组件执行 HTTP 调用。

如果我们在上面的示例中采用我的“选项 1”解决方案,我们将模拟 CustomerResource 中的 customerOrchestrator 字段,如果我们采用“选项 2”,我们将为 CustomerResource 中的每个依赖项注入 Impl 类(真正的类)依赖图并创建模拟数据库条目和模拟下游服务。

作为旁注,我应该提到我们很少在提供者测试中实际使用真实的数据库。在我们采用“选项 2”的情况下,我们模拟了 DAO 类层,而不是模拟实际的数据库数据,以减少测试中移动部件的数量。

我们创建了一个“测试框架”,它自动模拟任何未在 spring 上下文中显式声明的 Autowired 依赖项,因此存根/模拟对我们来说是一个轻量级的过程。这是一个提供者测试的摘录,该测试执行 CustomerResource 并启动存根的 CustomerOrchestrator bean:

@RunWith(PactRunner.class)
@Provider("customer-rest-api")
@PactCachedLoader(CustomerProviderContractTest.class)
public class CustomerProviderContractTest {

    @ClassRule
    public static PactJerseyWebbAppDescriptorRule webAppRule = buildWebAppDescriptorRule();

    @Rule
    public PactJerseyTestRule jersyTestRule = new PactJerseyTestRule(webAppRule.appDescriptor);

    @TestTarget public final Target target = new HttpTarget(jersyTestRule.port);

    private static PactJerseyWebbAppDescriptorRule buildWebAppDescriptorRule() {
        return PactJerseyWebbAppDescriptorRule.Builder.getBuilder()
            .withContextConfigLocation("classpath:applicationContext-test.xml")
            .withRestResourceClazzes(CustomerResource.class)
            .withPackages("api.rest.customer")
            .build();
    }

    @State("that customer with id 1111111 exists")
    public void state1() throws Exception {
        CustomerOrchestrator customerOrchestratorStub = SpringApplicationContext.getBean(CustomerOrchestrator.class)
       when(customerOrchestratorStub.getCustomer(eq("1111111"))).thenReturn(createMockedCustomer("1111111));

    }
    ...
4

3 回答 3

5

这是一个经常出现的问题,我的回答是“做对每项服务有意义的事情”。使用 pact 的第一个微服务是如此的小而简单,以至于在没有任何模拟或存根的情况下测试整个服务是最简单的。调用真实服务和验证测试调用之间的唯一区别是我们使用 sqlite 进行测试。当然,我们将对下游服务的调用存根。

如果设置真实数据比存根更复杂,那么我会使用存根。然而!如果您打算这样做,那么您需要确保您存根的调用以与协议相同的方式进行验证。使用某种共享固定装置,并确保对于您在协议提供者测试中存根的每个调用,您都有一个匹配的测试来确保行为符合您的预期。就像您将协作/合同测试链接在一起一样: 在此处输入图像描述

于 2016-12-06T04:22:59.287 回答
2

我说选择选项2。

原因是因为 Pact 存在的全部理由是对您的代码更改充满信心 - 这不会破坏消费者,或者如果确实如此,请找到一种方法来管理该更改(版本控制)同时仍然保持交互完好无损的。

为了完全自信,您必须尽可能多地使用“真实”代码,虽然数据可以是模拟的或真实的,但这并不重要。请记住,您希望能够在部署之前测试尽可能多的生产代码。

我使用它的方式,我有 2 种类型的测试,单元测试和 Pact 测试。单元测试确保我的代码不会因愚蠢的错误或错误的输入而中断,这非常适合模拟依赖项,而 Pact 测试用于测试消费者和提供者之间的交互,并且我的代码更改不会影响请求或数据格式。您可能会在此处模拟依赖项,但这可能会导致生产中断,因为该依赖项可能会影响请求或数据。

最后,这完全取决于您如何使用 Pact,只要您使用它来测试消费者和提供者之间的合同。

于 2016-12-08T02:53:03.830 回答
1

我们已经决定了选项 2。那就是我们应该努力在提供程序测试中包含尽可能多的真实代码。主要原因是,在补充组件测试中从模拟的 spring bean 实现测试对称性比使用选项 2 的结果稍微复杂一点的提供程序测试要复杂得多。

感谢您的投入!

于 2016-12-14T13:35:53.250 回答