jax-rs jax-ws
自从我们谈论测试和应用有效的TDD做法以来,已经有一段时间了,特别是与REST(ful) Web服务和API有关的做法。 但是,这个主题永远都不应忘记,特别是在每个人都在做微服务的世界中,无论它意味着什么,暗示或采取什么措施。
公平地说,基于微服务的体系结构在很多领域大放异彩 ,使组织可以更快地移动和创新。 但是如果没有适当的纪律,这也会使我们的系统变得脆弱,因为它们变得非常松散。 在今天的帖子中,我们将讨论基于合同的测试和消费者驱动的合同,这是一种实用且可靠的技术,可确保我们的微服务兑现其承诺。
那么, 基于合同的测试如何工作? 简而言之,它是一种非常简单的技术,并遵循以下步骤:
- 提供商(例如服务A )发布其联系人(或规范),则该实现可能在此阶段不可用
- 消费者(例如服务B )遵循此合同(或规范)以实现与服务A的对话
- 此外,消费者引入了一个测试套件,以验证其对服务A合同履行的期望
对于SOAP Web服务和API,事情很明显,因为以WSDL文件的形式存在显式契约。 但是在使用REST(ful) API的情况下,有很多不同的选择( WADL , RAML , Swagger …),并且仍然没有达成一致。 听起来可能很复杂,但请不要沮丧,因为Pact即将解救!
Pact是一系列框架,用于支持消费者驱动的合同测试。 有许多语言绑定和实现可用,包括JVM, JVM Pact和Scala-Pact 。 为了发展这种多语言生态系统, Pact还包括一个专用规范 ,以提供不同实现之间的互操作性。
太好了, Pact就在这里,阶段已经准备就绪,我们准备好迎接一些真实的代码片段。 让我们假设我们正在使用出色的Apache CXF和JAX-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}
}
目前,实现细节并不重要,但是让我们看一下GenericError , PersonUpdate和Person类,因为它们是我们服务合同不可分割的一部分。
@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中并分发给每个合作伙伴或感兴趣的消费者。
如果我们可以将此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_ID和CONSUMER_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;@Rulepublic 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
jax-rs jax-ws