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 .