Статьи

Интеграционное тестирование пользовательских ограничений проверки в Джерси 2

Недавно я присоединился к команде, пытающейся превратить монолитную унаследованную систему в набор служб 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-сервисы!