Перейти к основному содержимому

Тестирование приложений Quarkus

· 8 мин. чтения

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 будет автоматически добавлять префикс Path к LibraryResource ко всем запросам :

@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 {
}

Теперь давайте применим это к нашему тесту:

@QuarkusTransactionalTest
class BookRepositoryIntegrationTest

Как мы видим, поскольку тесты Quarkus представляют собой полноценные компоненты CDI , мы можем воспользоваться всеми преимуществами CDI, такими как внедрение зависимостей, контексты транзакций и перехватчики CDI.

6. Насмешка

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

6.1. @Насмехаться

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

6.2. @QuarkusMock

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

@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());
}
}

6.3. @InjectMock

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

@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());
}
}

6.4. @InjectSpy

Если нас интересует только слежка, а не замена поведения bean-компонента, мы можем использовать предоставленную аннотацию @InjectSpy :

@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");
}

}

7. Тестовые профили

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

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

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";
}
}

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

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

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

@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"));
}
}

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

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

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

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

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

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

mvn verify -Pnative

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

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

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

@QuarkusTestResource(OurCustomResourceImpl.class)

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

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

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

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

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