«1. Обзор

В наши дни Quarkus позволяет очень легко разрабатывать надежные и чистые приложения. Но как насчет тестирования?

В этом руководстве мы подробно рассмотрим, как можно протестировать приложение Quarkus. Мы изучим возможности тестирования, предлагаемые Quarkus, и представим такие концепции, как управление зависимостями и внедрение, имитация, конфигурация профиля и более конкретные вещи, такие как аннотации Quarkus и тестирование собственного исполняемого файла.

2. Настройка

Начнем с базового проекта Quarkus, настроенного в нашем предыдущем руководстве по QuarkusIO.

Сначала мы добавим зависимости Maven quarkus-reasteasy-jackson, quarkus-hibernate-orm-panache, quarkus-jdbc-h2, quarkus-junit5-mockito и quarkus-test-h2:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5-mockito</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-test-h2</artifactId>
</dependency>

~~ ~ Далее давайте создадим нашу доменную сущность:

public class Book extends PanacheEntity {
    private String title;
    private String author;
}

Мы продолжим, добавив простой репозиторий Panache с методом поиска книг:

public class BookRepository implements PanacheRepository {

    public Stream<Book> findBy(String query) {
        return find("author like :query or title like :query", with("query", "%"+query+"%")).stream();
    }
}

Теперь давайте напишем LibraryService для хранения любой бизнес-логики:

public class LibraryService {

    public Set<Book> find(String query) {
        if (query == null) {
            return bookRepository.findAll().stream().collect(toSet());
        }
        return bookRepository.findBy(query).collect(toSet());
    }
}

И, наконец, давайте представим функциональность нашего сервиса через HTTP, создав LibraryResource:

@Path("/library")
public class LibraryResource {

    @GET
    @Path("/book")
    public Set findBooks(@QueryParam("query") String query) {
        return libraryService.find(query);
    }
}

3. @Альтернативные реализации

Прежде чем писать какие-либо тесты, давайте удостоверимся, что в нашем репозитории есть книги. . С Quarkus мы можем использовать механизм CDI @Alternative, чтобы предоставить собственную реализацию bean-компонента для наших тестов. Давайте создадим TestBookRepository, который расширяет BookRepository:

@Priority(1)
@Alternative
@ApplicationScoped
public class TestBookRepository extends BookRepository {

    @PostConstruct
    public void init() {
        persist(new Book("Dune", "Frank Herbert"),
          new Book("Foundation", "Isaac Asimov"));
    }

}

Мы поместим этот альтернативный компонент в наш тестовый пакет, и благодаря аннотациям @Priority(1) и @Alternative мы уверены, что любой тест подберет его поверх фактическая реализация BookRepository. Это один из способов создать глобальный макет, который можно использовать во всех наших тестах Quarkus. Вскоре мы рассмотрим более узконаправленные макеты, а сейчас давайте перейдем к созданию нашего первого теста.

4. Тест интеграции HTTP

Давайте начнем с создания простого теста интеграции с поддержкой REST:

@QuarkusTest
class LibraryResourceIntegrationTest {

    @Test
    void whenGetBooksByTitle_thenBookShouldBeFound() {

        given().contentType(ContentType.JSON).param("query", "Dune")
          .when().get("/library/book")
          .then().statusCode(200)
          .body("size()", is(1))
          .body("title", hasItem("Dune"))
          .body("author", hasItem("Frank Herbert"));
    }
}

Этот тест, аннотированный @QuarkusTest, сначала запускает приложение Quarkus, а затем выполняет серию HTTP-запросов. против конечной точки нашего ресурса.

Теперь давайте воспользуемся некоторыми механизмами Quarkus, чтобы попытаться улучшить наш тест.

4.1. Внедрение URL-адреса с помощью @TestHTTPResource

Вместо того, чтобы жестко кодировать путь к нашей конечной точке HTTP, давайте введем URL-адрес ресурса:

@TestHTTPResource("/library/book")
URL libraryEndpoint;

И затем давайте использовать его в наших запросах:

given().param("query", "Dune")
  .when().get(libraryEndpoint)
  .then().statusCode(200);

Или, без использования Rest-assured, давайте просто откроем соединение с введенным URL-адресом и протестируем ответ:

@Test
void whenGetBooks_thenBooksShouldBeFound() throws IOException {
    assertTrue(IOUtils.toString(libraryEndpoint.openStream(), defaultCharset()).contains("Asimov"));
}

Как мы видим, внедрение URL-адреса @TestHTTPResource дает нам простой и гибкий способ доступа к нашей конечной точке.

4.2. @TestHTTPEndpoint

Давайте пойдем дальше и настроим нашу конечную точку, используя предоставленную Quarkus аннотацию @TestHTTPEndpoint:

@TestHTTPEndpoint(LibraryResource.class)
@TestHTTPResource("book")
URL libraryEndpoint;

Таким образом, если мы когда-нибудь решим изменить путь к LibraryResource, тест выберет правильный путь. без необходимости прикасаться к нему.

@TestHTTPEndpoint также можно применять на уровне класса, и в этом случае REST-assured будет автоматически добавлять перед всеми запросами путь к библиотечному ресурсу:

@QuarkusTest
@TestHTTPEndpoint(LibraryResource.class)
class LibraryHttpEndpointIntegrationTest {

    @Test
    void whenGetBooks_thenShouldReturnSuccessfully() {
        given().contentType(ContentType.JSON)
          .when().get("book")
          .then().statusCode(200);
    }
}

5. Внедрение контекста и зависимостей

Когда это произойдет для внедрения зависимостей, в тестах Quarkus мы можем использовать @Inject для любой необходимой зависимости. Давайте посмотрим на это в действии, создав тест для нашего LibraryService:

@QuarkusTest
class LibraryServiceIntegrationTest {

    @Inject
    LibraryService libraryService;

    @Test
    void whenFindByAuthor_thenBookShouldBeFound() {
        assertFalse(libraryService.find("Frank Herbert").isEmpty());
    }
}

Теперь давайте попробуем протестировать наш Panache BookRepository:

class BookRepositoryIntegrationTest {

    @Inject
    BookRepository bookRepository;

    @Test
    void givenBookInRepository_whenFindByAuthor_thenShouldReturnBookFromRepository() {
        assertTrue(bookRepository.findBy("Herbert").findAny().isPresent());
    }
}

Но когда мы запускаем наш тест, он терпит неудачу. Это потому, что он требует запуска в контексте транзакции, и ни один из них не активен. Это можно исправить, просто добавив @Transactional в тестовый класс. Или, если мы предпочитаем, мы можем определить наш собственный стереотип, чтобы связать как @QuarkusTest, так и @Transactional. Давайте сделаем это, создав аннотацию @QuarkusTransactionalTest:

@QuarkusTest
@Stereotype
@Transactional
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface QuarkusTransactionalTest {
}

Теперь давайте применим ее к нашему тесту: преимущества CDI, такие как внедрение зависимостей, контексты транзакций и перехватчики CDI.

@QuarkusTransactionalTest
class BookRepositoryIntegrationTest

6. Насмешки

«Имитация является критическим аспектом любого тестирования. Как мы уже видели выше, тесты Quarkus могут использовать механизм CDI @Alternative. Давайте теперь углубимся в возможности имитации, которые может предложить Quarkus.

6.1. @Mock

В качестве небольшого упрощения подхода @Alternative мы можем использовать аннотацию стереотипа @Mock. Это объединяет аннотации @Alternative и @Primary(1).

6.2. @QuarkusMock

Если мы не хотим иметь глобально определенный макет, а хотим, чтобы наш макет был только в рамках одного теста, мы можем использовать @QuarkusMock:

6.3. @InjectMock

@QuarkusTest
class LibraryServiceQuarkusMockUnitTest {

    @Inject
    LibraryService libraryService;

    @BeforeEach
    void setUp() {
        BookRepository mock = Mockito.mock(TestBookRepository.class);
        Mockito.when(mock.findBy("Asimov"))
          .thenReturn(Arrays.stream(new Book[] {
            new Book("Foundation", "Isaac Asimov"),
            new Book("I Robot", "Isaac Asimov")}));
        QuarkusMock.installMockForType(mock, BookRepository.class);
    }

    @Test
    void whenFindByAuthor_thenBooksShouldBeFound() {
        assertEquals(2, libraryService.find("Asimov").size());
    }
}

Давайте немного упростим ситуацию и воспользуемся аннотацией Quarkus @InjectMock вместо @QuarkusMock:

6.4. @InjectSpy

@QuarkusTest
class LibraryServiceInjectMockUnitTest {

    @Inject
    LibraryService libraryService;

    @InjectMock
    BookRepository bookRepository;

    @BeforeEach
    void setUp() {
        when(bookRepository.findBy("Frank Herbert"))
          .thenReturn(Arrays.stream(new Book[] {
            new Book("Dune", "Frank Herbert"),
            new Book("Children of Dune", "Frank Herbert")}));
    }

    @Test
    void whenFindByAuthor_thenBooksShouldBeFound() {
        assertEquals(2, libraryService.find("Frank Herbert").size());
    }
}

Если нас интересует только слежка, а не замена поведения bean-компонента, мы можем использовать предоставленную аннотацию @InjectSpy: конфигурации. Для этого Quarkus предлагает концепцию тестового профиля. Давайте создадим тест, который работает с другим механизмом базы данных, используя настроенную версию нашего BookRepository, и который также будет предоставлять наши ресурсы HTTP по пути, отличному от уже настроенного.

Для этого мы начнем с реализации QuarkusTestProfile:

@QuarkusTest
class LibraryResourceInjectSpyIntegrationTest {

    @InjectSpy
    LibraryService libraryService;

    @Test
    void whenGetBooksByAuthor_thenBookShouldBeFound() {
        given().contentType(ContentType.JSON).param("query", "Asimov")
          .when().get("/library/book")
          .then().statusCode(200);

        verify(libraryService).find("Asimov");
    }

}

Давайте теперь настроим наш application.properties, добавив свойство конфигурации пользовательского профиля, которое изменит наше хранилище H2 из памяти в файл:

~~ ~ Наконец, со всеми ресурсами и конфигурацией, давайте напишем наш тест:

Как видно из аннотации @TestProfile, этот тест будет использовать CustomTestProfile. Он будет отправлять HTTP-запросы к настраиваемой конечной точке, переопределяемой в методе профиля getConfigOverrides. Более того, он будет использовать альтернативную реализацию репозитория книг, настроенную в методе getEnabledAlternatives. И, наконец, с помощью пользовательского профиля, определенного в getConfigProfile, данные будут сохраняться в файле, а не в памяти.

public class CustomTestProfile implements QuarkusTestProfile {

    @Override
    public Map<String, String> getConfigOverrides() {
        return Collections.singletonMap("quarkus.resteasy.path", "/custom");
    }

    @Override
    public Set<Class<?>> getEnabledAlternatives() {
        return Collections.singleton(TestBookRepository.class);
    }

    @Override
    public String getConfigProfile() {
        return "custom-profile";
    }
}

Следует отметить, что Quarkus выключится, а затем перезапустится с новым профилем перед выполнением этого теста. Это добавляет некоторое время, когда происходит выключение/перезагрузка, но это цена, которую приходится платить за дополнительную гибкость.

%custom-profile.quarkus.datasource.jdbc.url = jdbc:h2:file:./testdb

8. Тестирование собственных исполняемых файлов

@QuarkusTest
@TestProfile(CustomBookRepositoryProfile.class)
class CustomLibraryResourceManualTest {

    public static final String BOOKSTORE_ENDPOINT = "/custom/library/book";

    @Test
    void whenGetBooksGivenNoQuery_thenAllBooksShouldBeReturned() {
        given().contentType(ContentType.JSON)
          .when().get(BOOKSTORE_ENDPOINT)
          .then().statusCode(200)
          .body("size()", is(2))
          .body("title", hasItems("Foundation", "Dune"));
    }
}

Quarkus предлагает возможность тестирования собственных исполняемых файлов. Давайте создадим тест собственного образа:

А теперь, запустив:

Мы увидим, как создается собственный образ и выполняются тесты для него.

Аннотация @NativeImageTest указывает Quarkus запустить этот тест для собственного образа, в то время как @QuarkusTestResource запустит экземпляр H2 в отдельный процесс перед началом теста. Последнее необходимо для запуска тестов с собственными исполняемыми файлами, поскольку ядро ​​базы данных не встроено в собственный образ.

@NativeImageTest
@QuarkusTestResource(H2DatabaseTestResource.class)
class NativeLibraryResourceIT extends LibraryHttpEndpointIntegrationTest {
}

Аннотацию @QuarkusTestResource также можно использовать для запуска пользовательских служб, таких как, например, Testcontainers. Все, что нам нужно сделать, это реализовать интерфейс QuarkusTestResourceLifecycleManager и аннотировать наш тест следующим образом:

mvn verify -Pnative

Вам понадобится GraalVM для создания нативного образа.

Также обратите внимание, что на данный момент внедрение не работает с собственным тестированием изображений. Единственное, что запускается изначально, — это приложение Quarkus, а не сам тест.

9. Заключение

@QuarkusTestResource(OurCustomResourceImpl.class)

В этой статье мы увидели, как Quarkus предлагает отличную поддержку для тестирования нашего приложения. От простых вещей, таких как управление зависимостями, внедрение и имитация, до более сложных аспектов, таких как профили конфигурации и собственные образы, Quarkus предоставляет нам множество инструментов для создания мощных и чистых тестов.

Как всегда, полный код доступен на GitHub.

«

In this article, we saw how Quarkus offers excellent support for testing our application. From simple things like dependency management, injection, and mocking, to more complex aspects like configuration profiles and native images, Quarkus provides us with many tools to create powerful and clean tests.

As always, the complete code is available over on GitHub.