Недавно я присоединился к команде, пытающейся превратить монолитную унаследованную систему в набор служб RESTful на Java. Они решили использовать последнюю версию 2.x Jersey в качестве контейнера REST, что не было для меня первым выбором, так как я не большой поклонник спецификаций JSR- *. Но теперь я должен признать, что JAX-RS 2.x делает все правильно: требует практически нулевого стандартного кода, поддерживает автоматическое обнаружение функций и предпочитает соглашения по конфигурации, как другие современные платформы. Поскольку спецификация еще молода, трудно найти хорошие учебные пособия и стартовые проекты с некоторым рабочим кодом. Я создал стартовый проект jersey2 на GitHub, который можно использовать в качестве отправной точки для собственного готового к работе сервиса RESTful. В этом посте я хотел бы осветить, как реализовать и проверить интеграцию ваших собственных ограничений валидации ресурсов REST.
Пользовательские ограничения
Одна из проблем, которая беспокоит меня при кодировании REST в Java, — это засорение модели вашего класса аннотациями. Предположим, что вы хотите создать простую службу REST списка Тодо, при использовании Jackson, валидации и Spring Data вы можете легко получить в качестве класса сущности:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
@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
ограничениями ( @NotNull
s). Некоторые могут сказать, что вы могли бы представить некоторый слой DTO с собственными правилами валидации, но для меня это противоречит чисто REST API, что означает, что вы работаете с ресурсами, которые должны отображаться на ваши доменные классы. С другой стороны — что это значит, что объект Todo
действителен ? Когда вы создаете Todo
вы должны предоставить описание и дату завершения, но что, когда вы обновляете? Вы должны иметь возможность изменить любое из описания, даты выполнения (отсрочки) и флага завершения (пометить как выполненные), но вы должны предоставить хотя бы один из них в качестве действительной модификации. Поэтому моя идея состоит в том, чтобы ввести пользовательские ограничения проверки, различные для создания и модификации:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
@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:
1
2
3
4
5
6
7
|
@POST @Consumes (APPLICATION_JSON) public Response create( @ValidForCreation Todo todo) {...} @PUT @Consumes (APPLICATION_JSON) public Response update( @ValidForModification Todo todo) {...} |
И теперь вы можете удалить эти NotNull
с вашей модели.
Интеграционное тестирование
В целом существует два подхода к интеграционному тестированию:
- тест выполняется на отдельной JVM, а не в приложении, которое развернуто в другой среде интеграции
- test развертывает приложение программно в блоке настройки.
У обоих есть свои плюсы и минусы, но для достаточно небольших услуг я лично предпочитаю второй подход. Это намного проще в настройке, и у вас есть только одна запущенная JVM, что делает отладку действительно простой. Вы можете использовать универсальный фреймворк, такой как Arquillian, для запуска вашего приложения в контейнерной среде, но я предпочитаю простые решения и просто использую добавленную Jetty. Чтобы сделать тестовую настройку 100% производственным эквивалентом, я создаю полный WebAppContext
Jetty и должен разрешить все зависимости времени выполнения для автоматического обнаружения Джерси. Это может быть просто достигнуто с помощью Maven, разрешенного из Shrinkwrap — подпроекта Arquillian:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
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 от Pragmatists. Это действительно просто и имеет хорошую концепцию JQuery-подобного помощника для создания провайдеров. Вот мой тестовый код (здесь я также использую шаблон для создания различных типов Todos):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
@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() ); } |
Хорошо, хватит читать, не стесняйтесь клонировать проект и начать писать свои REST-сервисы!