1. Overview
1.概述
Quarkus makes it very easy these days to develop robust and clean applications. But how about testing?
如今,Quarkus使开发健壮和干净的应用程序变得非常容易。但测试呢?
In this tutorial, we’ll take a close look at how a Quarkus application can be tested. We’ll explore the testing possibilities offered by Quarkus and present concepts like dependency management and injection, mocking, profile configuration, and more specific things like Quarkus annotations and testing a native executable.
在本教程中,我们将仔细研究如何对Quarkus应用程序进行测试。我们将探索Quarkus提供的测试可能性,并介绍依赖管理和注入、嘲弄、配置文件配置等概念,以及Quarkus注释和测试本地可执行文件等更具体的东西。
2. Setup
2.设置
Let’s start from the basic Quarkus project configured in our previous Guide to QuarkusIO.
让我们从之前QuarkusIO指南中配置的基本Quarkus项目开始。
First, we’ll add the quarkus-reasteasy-jackson, quarkus-hibernate-orm-panache, quarkus-jdbc-h2, quarkus-junit5-mockito, and quarkus-test-h2 Maven dependencies:
首先,我们将添加quarkus-reasteasy-jackson、quarkus-hibernate-orm-panache、quarkus-jdbc-h2、quarkus-junit5-mockito和quarkus-test-h2Maven依赖项。
<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>
Next, let’s create our domain entity:
接下来,让我们创建我们的域实体。
public class Book extends PanacheEntity {
private String title;
private String author;
}
We continue by adding a simple Panache repository, with a method to search for books:
我们继续添加一个简单的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();
}
}
Now, let’s write a LibraryService to hold any business logic:
现在,让我们写一个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());
}
}
And finally, let’s expose our service functionality through HTTP by creating a LibraryResource:
最后,让我们通过创建一个LibraryResource,通过HTTP暴露我们的服务功能。
@Path("/library")
public class LibraryResource {
@GET
@Path("/book")
public Set findBooks(@QueryParam("query") String query) {
return libraryService.find(query);
}
}
3. @Alternative Implementations
3.@Alternative Implementations
Before writing any tests, let’s make sure we have some books in our repository. With Quarkus, we can use the CDI @Alternative mechanism to provide a custom bean implementation for our tests. Let’s create a TestBookRepository that extends BookRepository:
在编写任何测试之前,让我们确保我们的仓库中有一些书。通过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"));
}
}
We place this alternative bean in our test package, and because of the @Priority(1) and @Alternative annotations, we’re sure any test will pick it up over the actual BookRepository implementation. This is one way we can provide a global mock that all our Quarkus tests can use. We’ll explore more narrow-focused mocks shortly, but now, let’s move on to creating our first test.
我们将这个替代Bean放在我们的test包中,由于@Priority(1)和@Alternative注解,我们确信任何测试都会选择它而不是实际的BookRepository实现。这是我们提供全局模拟的一种方式,我们所有的Quarkus测试都可以使用。我们很快就会探索更多狭义的模拟,但现在,让我们继续创建我们的第一个测试。
4. HTTP Integration Test
4.HTTP集成测试
Let’s begin by creating a simple REST-assured integration test:
让我们从创建一个简单的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"));
}
}
This test, annotated with @QuarkusTest, first starts the Quarkus application and then performs a series of HTTP requests against our resource’s endpoint.
这个测试,用@QuarkusTest注释,首先启动Quarkus应用程序,然后对我们的资源端点执行一系列的HTTP请求。
Now, let’s make use of some Quarkus mechanisms to try and further improve our test.
现在,让我们利用一些Quarkus机制来尝试进一步改进我们的测试。
4.1. URL Injection With @TestHTTPResource
4.1.用@TestHTTPResource进行URL注入
Instead of hard-coding the path of our HTTP endpoint, let’s inject the resource URL:
不要硬编码我们的HTTP端点的路径,让我们注入资源的URL。
@TestHTTPResource("/library/book")
URL libraryEndpoint;
And then, let’s use it in our requests:
然后,让我们在我们的请求中使用它。
given().param("query", "Dune")
.when().get(libraryEndpoint)
.then().statusCode(200);
Or, without using Rest-assured, let’s simply open a connection to the injected URL and test the response:
或者,不使用Rest-assured,让我们简单地打开一个连接到注入的URL并测试响应。
@Test
void whenGetBooks_thenBooksShouldBeFound() throws IOException {
assertTrue(IOUtils.toString(libraryEndpoint.openStream(), defaultCharset()).contains("Asimov"));
}
As we can see, @TestHTTPResource URL injection gives us an easy and flexible way of accessing our endpoint.
正如我们所看到的,@TestHTTPResource URL注入给了我们一个简单而灵活的方式来访问我们的端点。
4.2. @TestHTTPEndpoint
4.2.@TestHTTPEndpoint
Let’s take this further and configure our endpoint using the Quarkus provided @TestHTTPEndpoint annotation:
让我们更进一步,使用Quarkus提供的@TestHTTPEndpoint注释来配置我们的端点。
@TestHTTPEndpoint(LibraryResource.class)
@TestHTTPResource("book")
URL libraryEndpoint;
This way, if we ever decide to change the path of the LibraryResource, the test will pick up the correct path without us having to touch it.
这样,如果我们决定改变LibraryResource的路径,测试将拾起正确的路径,而不需要我们去碰它。
@TestHTTPEndpoint can also be applied at the class level, in which case REST-assured will automatically prefix all requests with the Path of the 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. Context and Dependency Injection
5.上下文和依赖性注入
When it comes to dependency injection, in Quarkus tests, we can use @Inject for any required dependency. Let’s see this in action by creating a test for our LibraryService:
当涉及到依赖注入时,在Quarkus测试中,我们可以对任何需要的依赖使用@Inject。让我们通过为我们的LibraryService创建一个测试来看看这个动作。
@QuarkusTest
class LibraryServiceIntegrationTest {
@Inject
LibraryService libraryService;
@Test
void whenFindByAuthor_thenBookShouldBeFound() {
assertFalse(libraryService.find("Frank Herbert").isEmpty());
}
}
Now, let’s try to test our Panache BookRepository:
现在,让我们试着测试一下我们的Panache BookRepository。
class BookRepositoryIntegrationTest {
@Inject
BookRepository bookRepository;
@Test
void givenBookInRepository_whenFindByAuthor_thenShouldReturnBookFromRepository() {
assertTrue(bookRepository.findBy("Herbert").findAny().isPresent());
}
}
But when we run our test, it fails. That’s because it requires running within the context of a transaction and there is none active. This can be fixed simply by adding @Transactional to the test class. Or, if we prefer, we can define our own stereotype to bundle both @QuarkusTest and @Transactional. Let’s do this by creating the @QuarkusTransactionalTest annotation:
但是当我们运行我们的测试时,它失败了。这是因为它要求在一个事务的上下文中运行,而现在没有活动的事务。这可以通过在测试类中添加@Transactional来解决。或者,如果我们愿意,我们可以定义我们自己的定型来捆绑@QuarkusTest和@Transactional。让我们通过创建@QuarkusTransactionalTest注解来做到这一点。
@QuarkusTest
@Stereotype
@Transactional
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface QuarkusTransactionalTest {
}
Now, let’s apply it to our test:
现在,让我们把它应用于我们的测试。
@QuarkusTransactionalTest
class BookRepositoryIntegrationTest
As we can see, because Quarkus tests are full CDI beans, we can take advantage of all the CDI benefits like dependency injection, transactional contexts, and CDI interceptors.
正如我们所看到的,因为Quarkus测试是完整的CDI beans,我们可以利用所有的CDI优势,如依赖注入、事务性上下文和CDI拦截器。
6. Mocking
6.嘲讽
Mocking is a critical aspect of any testing effort. As we’ve already seen above, Quarkus tests can make use of the CDI @Alternative mechanism. Let’s now dive deeper into the mocking capabilities Quarkus has to offer.
嘲弄是任何测试工作的一个重要方面。正如我们在上面已经看到的,Quarkus测试可以利用CDI @Alternative机制。现在让我们更深入地了解Quarkus所提供的嘲弄功能。
6.1. @Mock
6.1@Mock
As a slight simplification of the @Alternative approach, we can use the @Mock stereotype annotation. This bundles together the @Alternative and @Primary(1) annotations.
作为@Alternative方法的轻微简化,我们可以使用@Mock定型注解。这将@Alternative和@Primary(1)注解捆绑起来。
6.2. @QuarkusMock
6.2@QuarkusMock
If we don’t want to have a globally defined mock, but would rather have our mock only within the scope of one test, we can use @QuarkusMock:
如果我们不想有一个全局定义的mock,而是想让我们的mock只在一个测试的范围内,我们可以使用@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
6.3.@InjectMock
Let’s simplify things a bit and use the Quarkus @InjectMock annotation instead of @QuarkusMock:
让我们把事情简化一下,使用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
6.4.@InjectSpy
If we’re only interested in spying and not replacing bean behavior, we can use the provided @InjectSpy annotation:
如果我们只对监视感兴趣,而不是替换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. Test Profiles
7.测试简介
We might want to run our tests in different configurations. For this, Quarkus offers the concept of a test profile. Let’s create a test that runs against a different database engine using a customized version of our BookRepository, and that will also expose our HTTP resources at a different path from the one already configured.
我们可能希望以不同的配置运行我们的测试。为此,Quarkus提供了一个测试配置文件的概念。让我们创建一个测试,使用自定义版本的BookRepository,针对不同的数据库引擎运行,并且还将在不同的路径上暴露我们的HTTP资源,而不是已经配置好的。
For this, we start by implementing a QuarkusTestProfile:
为此,我们首先实现一个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";
}
}
Let’s now configure our application.properties by adding a custom-profile config property that will change our H2 storage from memory to file:
现在让我们配置我们的application.properties,添加一个custom-profile配置属性,将我们的H2存储从内存改为文件。
%custom-profile.quarkus.datasource.jdbc.url = jdbc:h2:file:./testdb
Finally, with all the resources and configuration in place, let’s write our test:
最后,在所有的资源和配置到位的情况下,让我们来编写我们的测试。
@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"));
}
}
As we can see from the @TestProfile annotation, this test will use the CustomTestProfile. It will make HTTP requests to the custom endpoint overridden in the profile’s getConfigOverrides method. Moreover, it will use the alternative book repository implementation configured in the getEnabledAlternatives method. And finally, by using the custom-profile defined in getConfigProfile, it will persist data in a file rather than memory.
正如我们从@TestProfile注解中看到的,这个测试将使用CustomTestProfile。它将向配置文件的getConfigOverrides方法中覆盖的自定义端点发出HTTP请求。此外,它将使用在getEnabledAlternatives方法中配置的替代书库实现。最后,通过使用getConfigProfile中定义的custom-profile,它将在一个文件中而不是在内存中保存数据。
One thing to note is that Quarkus will shut down and then restart with the new profile before this test is executed. This adds some time as the shutdown/restart happens, but it’s the price to be paid for the extra flexibility.
有一点需要注意的是,Quarkus会在执行这个测试之前关闭,然后用新的配置文件重新启动。这在关机/重启过程中会增加一些时间,但这是为额外的灵活性所付出的代价。
8. Testing Native Executables
8.测试本地可执行文件
Quarkus offers the possibility to test native executables. Let’s create a native image test:
Quarkus提供了测试本地可执行文件的可能性。让我们创建一个本地图像测试。
@NativeImageTest
@QuarkusTestResource(H2DatabaseTestResource.class)
class NativeLibraryResourceIT extends LibraryHttpEndpointIntegrationTest {
}
And now, by running:
而现在,通过跑步。
mvn verify -Pnative
We’ll see the native image being built and the tests running against it.
我们将看到本地镜像的建立和针对它的测试运行。
The @NativeImageTest annotation instructs Quarkus to run this test against the native image, while the @QuarkusTestResource will start an H2 instance into a separate process before the test begins. The latter is needed for running tests against native executables as the database engine is not embedded into the native image.
@NativeImageTest 注解指示Quarkus针对本地图像运行该测试,而@QuarkusTestResource 将在测试开始前启动一个H2实例到一个单独的进程中。后者是针对本地可执行文件运行测试所需要的,因为数据库引擎没有被嵌入到本地图像中。
The @QuarkusTestResource annotation can also be used to start custom services, like Testcontainers, for example. All we need to do is implement the QuarkusTestResourceLifecycleManager interface and annotate our test with:
@QuarkusTestResource注解也可用于启动自定义服务,例如Testcontainers。我们需要做的就是实现QuarkusTestResourceLifecycleManager接口,并为我们的测试加上注解。
@QuarkusTestResource(OurCustomResourceImpl.class)
You will need a GraalVM for building the native image.
你将需要一个GraalVM来构建本地镜像。
Also, take notice that, at the moment, injection does not work with native image testing. The only thing that runs natively is the Quarkus application, not the test itself.
另外,请注意,目前,注入法不能与原生图像测试一起使用。本地运行的唯一东西是Quarkus应用程序,而不是测试本身。
9. Conclusion
9.结语
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.
在这篇文章中,我们看到了Quarkus如何为测试我们的应用程序提供出色的支持。从简单的依赖管理、注入和嘲弄,到更复杂的方面,如配置文件和本地图像,Quarkus为我们提供了许多工具来创建强大和干净的测试。
As always, the complete code is available over on GitHub.
一如既往,完整的代码可在GitHub上获得,。