契约提供者测试的范围应该是什么?

2022-09-02 10:55:30

大约半年前,我的组织开始使用 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依赖关系,而这些依赖关系反过来可能还有其他...等。最后,依赖项将最终出现在将进行数据库调用的 DAO 对象中,或者位于将对其他下游服务/组件执行 HTTP 调用的 REST 客户端中。

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

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

我们创建了一个“测试框架”,它会自动模拟任何在spring上下文中未显式声明的Autowired依赖项,因此stubbing/mocking对我们来说是一个轻量级的过程。这是一个提供程序测试的摘录,该测试执行 CustomerResource 并启动 stubbed 的 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));

    }
    ...

答案 1

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

如果设置真实数据比存根更复杂,那么我会使用存根。但是!如果您要执行此操作,则需要确保以与协议相同的方式验证您存根的调用。使用某种共享固定装置,并确保对于在约定提供程序测试中存根的每个调用,您都有一个匹配测试,以确保行为符合预期。这就像您将协作/合同测试链接在一起,如下所示:enter image description here


答案 2

我说选择选项2。

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

为了完全自信,你必须尽可能多地使用“真实”代码,虽然数据可以被嘲笑或真实,但在这一点上并不重要。请记住,您希望能够在部署生产代码之前对其进行尽可能多的测试。

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

不过,归根结底,这完全取决于你如何使用Pact的偏好,只要你用它来测试消费者和提供者之间的契约。


推荐