信守承诺:JAX-RS API的基于合同的测试

自从我们谈论测试和应用有效的TDD实践以来已经有一段时间了,特别是与REST(ful) Web服务和API相关的实践。 但是,这个主题永远都不应忘记,特别是在每个人都在做微服务的世界中,无论它意味着什么,暗示或采取什么措施。

公平地说,基于微服务的体系结构在很多领域大放异彩 ,使组织可以更快地移动和创新。 但是如果没有适当的纪律,这也会使我们的系统变得脆弱,因为它们变得非常松散。 在今天的帖子中,我们将讨论基于合同的测试消费者驱动的合同,这是一种实用且可靠的技术,可确保我们的微服务兑现其承诺。

那么, 基于合同的测试如何工作? 简而言之,这是一种非常简单的技术,并遵循以下步骤:

  • 提供商(例如服务A )发布其联系方式(或规范),则在此阶段可能甚至无法使用该实现
  • 消费者(例如服务B )遵循此合同(或规范)以实现与服务A的对话
  • 此外,消费者引入了一个测试套件,以验证其对服务A合同履行的期望

对于SOAP Web服务和API,事情很明显,因为以WSDL文件的形式存在明确的契约。 但是在使用REST(ful) API的情况下,拐角处有很多不同的选择( WADLRAMLSwagger …),并且仍然没有达成一致。 听起来可能很复杂,但请不要沮丧,因为Pact即将解救!

Pact是支持消费者驱动的合同测试的一系列框架。 有许多语言绑定和实现可用,包括JVM, JVM PactScala-Pact 。 为了发展这种多语言生态系统, Pact还包括一个专用规范 ,以提供不同实现之间的互操作性。

太好了, Pact就在这里,阶段已经准备就绪,我们准备好迎接一些真实的代码片段。 让我们假设我们正在使用出色的Apache CXFJAX-RS 2.0规范开发用于管理人员的REST(ful) Web API。 为简单起见,我们将仅介绍两个端点:

  • POST / people / v1创建新的人
  • GET / people / v1?email = <email>通过电子邮件地址查找人

从本质上讲,我们可能不会打扰他们,而只是将我们的服务合同中的这些最小部分传达给每个人,因此,让消费者自己应对(事实上, Pact支持这种情况)。 但是可以肯定的是,我们不是那样的,我们确实在乎,并且想全面地记录我们的API,可能我们已经很熟悉Swagger了 。 这样,这就是我们的PeopleRestService

@Api(value = "Manage people")
@Path("/people/v1")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class PeopleRestService {
    @GET
    @ApiOperation(value = "Find person by e-mail", 
        notes = "Find person by e-mail", response = Person.class)
    @ApiResponses({
        @ApiResponse(code = 404, 
            message = "Person with such e-mail doesn't exists", 
            response = GenericError.class)
    })
    public Response findPerson(
        @ApiParam(value = "E-Mail address to lookup for", required = true) 
        @QueryParam("email") final String email) {
        // implementation here
    }

    @POST
    @ApiOperation(value = "Create new person", 
        notes = "Create new person", response = Person.class)
    @ApiResponses({
        @ApiResponse(code = 201, 
            message = "Person created successfully", 
            response = Person.class),
        @ApiResponse(code = 409, 
            message = "Person with such e-mail already exists", 
            response = GenericError.class)
    })
    public Response addPerson(@Context UriInfo uriInfo, 
        @ApiParam(required = true) PersonUpdate person) {
        // implementation here
    }
}

目前,实现细节并不重要,但是让我们看一下GenericErrorPersonUpdatePerson类,因为它们是我们服务合同不可分割的一部分。

@ApiModel(description = "Generic error representation")
public class GenericError {
    @ApiModelProperty(value = "Error message", required = true)
    private String message;
}

@ApiModel(description = "Person resource representation")
public class PersonUpdate {
    @ApiModelProperty(value = "Person's first name", required = true) 
    private String email;
    @ApiModelProperty(value = "Person's e-mail address", required = true) 
    private String firstName;
    @ApiModelProperty(value = "Person's last name", required = true) 
    private String lastName;
    @ApiModelProperty(value = "Person's age", required = true) 
    private int age;
}

@ApiModel(description = "Person resource representation")
public class Person extends PersonUpdate {
    @ApiModelProperty(value = "Person's identifier", required = true) 
    private String id;
}

优秀的! 一旦我们有了Swagger批注并且打开了Apache CXF Swagger集成 ,我们就可以生成swagger.json规范文件,将其置于Swagger UI中并分发给每个合作伙伴或感兴趣的消费者。

人们休息服务2

人们休息服务1

如果我们可以将此Swagger规范与Pact框架实现一起用作服务合同,那就太好了。 感谢Atlassian ,我们当然可以使用swagger-request-validator来做到这一点, swagger-request-validator是一个用于根据Swagger / OpenAPI规范验证HTTP请求/响应的库,该库也很好地与Pact JVM集成在一起。

太酷了,现在让我们从提供商转向消费者,并尝试找出拥有这样的Swagger规范可以做什么。 事实证明,我们可以做很多事情。 例如,让我们看一下创建新人员的POST操作。 作为客户(或消费者),我们可以用以下形式表达我们的期望:与请求一起提交有效的有效载荷,我们期望提供者返回HTTP状态代码201 ,并且响应有效载荷应该包含一个新的人。分配的标识符。 实际上,将此语句转换为Pact JVM断言非常简单。

@Pact(provider = PROVIDER_ID, consumer = CONSUMER_ID)
public PactFragment addPerson(PactDslWithProvider builder) {
    return builder
        .uponReceiving("POST new person")
        .method("POST")
        .path("/services/people/v1")
        .body(
            new PactDslJsonBody()
                .stringType("email")
                .stringType("firstName")
                .stringType("lastName")
                .numberType("age")
        )
        .willRespondWith()
        .status(201)
        .matchHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
        .body(
            new PactDslJsonBody()
                .uuid("id")
                .stringType("email")
                .stringType("firstName")
                .stringType("lastName")
                .numberType("age")
        )
       .toFragment();
}

为了触发合同验证过程,我们将使用很棒的JUnit和非常流行的REST保证框架。 但是在此之前,让我们从上面的代码片段中阐明什么是PROVIDER_IDCONSUMER_ID 。 如您所料, PROVIDER_ID是合同规范的参考。 为简单起见,我们将从运行PeopleRestService端点获取Swagger规范,幸运的是, Spring Boot测试的改进使此任务变得轻而易举。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, 
    classes = PeopleRestConfiguration.class)
public class PeopleRestContractTest {
    private static final String PROVIDER_ID = "People Rest Service";
    private static final String CONSUMER_ID = "People Rest Service Consumer";

    private ValidatedPactProviderRule provider;
    @Value("${local.server.port}")
    private int port;

    @Rule
    public ValidatedPactProviderRule getValidatedPactProviderRule() {
        if (provider == null) {
            provider = new ValidatedPactProviderRule("http://localhost:" + port + 
                "/services/swagger.json", null, PROVIDER_ID, this);
        }

        return provider;
    }
}

CONSUMER_ID只是识别消费者的一种方式,对此不多说。 这样,我们准备完成第一个测试用例:

@Test
@PactVerification(value = PROVIDER_ID, fragment = "addPerson")
public void testAddPerson() {
    given()
        .contentType(ContentType.JSON)
        .body(new PersonUpdate("tom@smith.com", "Tom", "Smith", 60))
        .post(provider.getConfig().url() + "/services/people/v1");
}

太棒了! 如此简单,只要注意@PactVerification批注的存在,我们就可以通过名称引用相应的验证片段,在这种情况下,它指出了我们之前介绍的addPerson方法。

很好,但是...有什么意义呢? 很高兴您提出这样的要求,因为从现在开始,合同中可能无法向后兼容的任何变更都将破坏我们的测试用例。 例如,如果提供者决定从响应有效负载中删除id属性,则测试用例将失败。 重命名请求有效负载属性,大不,再次,测试用例将失败。 添加新的路径参数? 运气不好,测试用例不能通过。 您可能会更进一步,即使每次向后兼容(即使使用swagger-validator.properties进行微调),每次合同更改也会失败。

validation.response=ERROR
validation.response.body.missing=ERROR

没有一个很好的主意,但是如果您需要它,它仍然在那里。 同样,让我们​​从成功的场景开始,为要寻找的人添加一些其他的GET端点测试用例,例如:

@Pact(provider = PROVIDER_ID, consumer = CONSUMER_ID)
public PactFragment findPerson(PactDslWithProvider builder) {
    return builder
        .uponReceiving("GET find person")
        .method("GET")
        .path("/services/people/v1")
        .query("email=tom@smith.com")
        .willRespondWith()
        .status(200)
        .matchHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
        .body(
            new PactDslJsonBody()
                .uuid("id")
                .stringType("email")
                .stringType("firstName")
                .stringType("lastName")
                .numberType("age")
        )
        .toFragment();
}

@Test
@PactVerification(value = PROVIDER_ID, fragment = "findPerson")
public void testFindPerson() {
    given()
        .contentType(ContentType.JSON)
        .queryParam("email", "tom@smith.com")
        .get(provider.getConfig().url() + "/services/people/v1");
}

请注意,这里我们引入了使用query(“ email=tom@smith.com”)断言的查询字符串验证。 根据可能的结果,让我们还介绍一下不成功的情况,即人员不存在,并且我们期望返回一些错误以及404状态代码,例如:

@Pact(provider = PROVIDER_ID, consumer = CONSUMER_ID)
public PactFragment findNonExistingPerson(PactDslWithProvider builder) {
    return builder
        .uponReceiving("GET find non-existing person")
        .method("GET")
        .path("/services/people/v1")
        .query("email=tom@smith.com")
        .willRespondWith()
        .status(404)
        .matchHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
        .body(new PactDslJsonBody().stringType("message"))
        .toFragment();
}

@Test
@PactVerification(value = PROVIDER_ID, fragment = "findNonExistingPerson")
public void testFindPersonWhichDoesNotExist() {
    given()
        .contentType(ContentType.JSON)
        .queryParam("email", "tom@smith.com")
        .get(provider.getConfig().url() + "/services/people/v1");
}

真正出色,可维护,可理解且非侵入性的方法,可以解决诸如基于合同的测试和由消费者驱动的合同之类的复杂而重要的问题。 希望这种有点新的测试技术可以帮助您在开发阶段捕获更多问题,从而避免它们有机会泄漏到生产中。

感谢Swagger,我们能够采取一些捷径,但是如果您没有这么奢侈的话, Pact会提供相当丰富的规格,非常欢迎您学习和使用。 无论如何, Pact JVM在帮助您编写小型而简洁的测试用例方面做得非常出色。

完整的项目资源可在Github上找到

翻译自: https://www.javacodegeeks.com/2016/11/keep-promises-contract-based-testing-jax-rs-apis.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值