我最近加入了一个团队,试图将一个单一的遗留系统转换为Java中的RESTful服务集。 他们决定使用最新的2.x版本的Jersey作为REST容器,这对我来说不是首选,因为我不是JSR- *规范的忠实拥护者。 但是现在我必须承认,JAX-RS 2.x的运行正确:需要几乎零的样板代码,支持功能的自动发现,并且像其他现代框架一样,更喜欢使用约定而不是配置。 由于该规范还很年轻,因此很难找到具有一些可用代码的优秀教程和启动项目。 我在GitHub上创建了jersey2-starter项目,该项目可用作您自己的生产就绪RESTful服务的起点。 在本文中,我想介绍如何实现和集成测试您自己的REST资源验证约束。
自定义约束
用Java编写REST时困扰我的问题之一是用注释乱扔您的类模型。 假设您想构建一个简单的Todo列表REST服务,当使用Jackson,validation和Spring Data时,您可以轻松地将其作为实体类结束:
@Document
public class Todo {
private Long id;
@NotNull
private String description;
@NotNull
private Boolean completed;
@NotNull
private DateTime dueDate;
@JsonCreator
public Todo(@JsonProperty("description") String description, @JsonProperty("dueDate") DateTime dueDate) {
this.description = description;
this.dueDate = dueDate;
this.completed = false;
}
// getters and setters
}
您的域模型现在几乎到处都被混乱的注释有效地模糊了。 让我们看看如何使用验证约束( @NotNull
)。 有人可能会说您可以引入一些具有自己的验证规则的DTO层,但是这与纯REST API设计对我来说是冲突的,后者表示您在应映射到您的域类的资源上进行操作。 另一方面– Todo
对象有效是什么意思? 创建Todo
,应提供说明和截止日期,但是更新时会怎样? 您应该能够更改描述,截止日期(推迟)和完成标志(标记为完成)中的任何一个,但是您至少应提供其中之一作为有效的修改。 所以我的想法是引入自定义验证约束,用于创建和修改的约束不同:
@Target({TYPE, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = ValidForCreation.Validator.class)
public @interface ValidForCreation {
//...
class Validator implements ConstraintValidator<ValidForCreation, Todo> {
/...
@Override
public boolean isValid(Todo todo, ConstraintValidatorContext constraintValidatorContext) {
return todo != null
&& todo.getId() == null
&& todo.getDescription() != null
&& todo.getDueDate() != null;
}
}
}
@Target({TYPE, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = ValidForModification.Validator.class)
public @interface ValidForModification {
//...
class Validator implements ConstraintValidator<ValidForModification, Todo> {
/...
@Override
public boolean isValid(Todo todo, ConstraintValidatorContext constraintValidatorContext) {
return todo != null
&& todo.getId() == null
&& (todo.getDescription() != null || todo.getDueDate() != null || todo.isCompleted() != null);
}
}
}
现在,您可以将验证注释移至REST端点的定义:
@POST
@Consumes(APPLICATION_JSON)
public Response create(@ValidForCreation Todo todo) {...}
@PUT
@Consumes(APPLICATION_JSON)
public Response update(@ValidForModification Todo todo) {...}
现在,您可以从模型中删除那些NotNull
。
整合测试
集成测试通常有两种方法:
- 测试是在与应用程序不同的JVM上运行的,而该应用程序已部署在其他集成环境中
- 测试在设置块中以编程方式部署应用程序。
两者都有其优点和缺点,但是对于足够小的服务,我个人更喜欢第二种方法。 设置起来非常容易,并且您只启动了一个JVM,这使得调试非常容易。 您可以使用Arquillian之类的通用框架在容器环境中启动应用程序,但是我更喜欢简单的解决方案,而只是使用嵌入式Jetty。 为了使测试设置100%等效于生产,我正在创建完整的Jetty的WebAppContext
并且必须解决所有运行时相关性,才能使Jersey自动发现正常工作。 这可以通过Arrinklian子项目Shrinkwrap解决的Maven 轻松实现:
WebAppContext webAppContext = new WebAppContext();
webAppContext.setResourceBase("src/main/webapp");
webAppContext.setContextPath("/");
File[] mavenLibs = Maven.resolver().loadPomFromFile("pom.xml")
.importCompileAndRuntimeDependencies()
.resolve().withTransitivity().asFile();
for (File file: mavenLibs) {
webAppContext.getMetaData().addWebInfJar(new FileResource(file.toURI()));
}
webAppContext.getMetaData().addContainerResource(new FileResource(new File("./target/classes").toURI()));
webAppContext.setConfigurations(new Configuration[] {
new AnnotationConfiguration(),
new WebXmlConfiguration(),
new WebInfConfiguration()
});
server.setHandler(webAppContext);
( 此Stackoverflow线程在这里启发了我很多)
现在是该文章最后一部分的时候了:参数化我们的集成测试。 由于我们要测试验证约束,因此有许多要检查的边缘路径(并使您的代码覆盖率接近100%)。 每个案例编写一个测试可能是一个坏主意。 在实用的JUnit解决方案中,我最相信实用主义者团队的Junit Params 。 它非常简单,并且具有用于创建提供程序的类似于JQuery的帮助程序的概念。 这是我的测试代码(我也在这里使用构建器模式来创建各种Todos):
@Test
@Parameters(method = "provideInvalidTodosForCreation")
public void shouldRejectInvalidTodoWhenCreate(Todo todo) {
Response response = createTarget().request().post(Entity.json(todo));
assertThat(response.getStatus()).isEqualTo(BAD_REQUEST.getStatusCode());
}
private static Object[] provideInvalidTodosForCreation() {
return $(
new TodoBuilder().withDescription("test").build(),
new TodoBuilder().withDueDate(DateTime.now()).build(),
new TodoBuilder().withId(123L).build(),
new TodoBuilder().build()
);
}
OK,足够的阅读了,随时克隆项目并开始编写REST服务!