云原生-Quarkus测试应用程序

483 阅读9分钟

1、回顾JVM模式下基于HTTP的测试

在 pom.xml 文件中,有2个测试依赖项:

`<dependency>`
 `<groupId>io.quarkus</groupId>`
 `<artifactId>quarkus-junit5</artifactId>`
 `<scope>test</scope>`
`</dependency>`
`<dependency>`
 `<groupId>io.rest-assured</groupId>`
 `<artifactId>rest-assured</artifactId>`
 `<scope>test</scope>`
`</dependency>`

quarkus-junit5测试是必需的,因为它提供了@QuarkusTest控制测试框架的注释。 rest-assured不是必需的,但它是测试HTTP端点的便捷方法,还提供了集成功能,该功能会自动设置正确的URL,因此无需进行配置。

因为使用的是JUnit 5,所以 必须设置Surefire Maven插件的版本,因为默认版本不支持Junit 5:

`<plugin>`
 `<artifactId>maven-surefire-plugin</artifactId>`
 `<version>${surefire-plugin.version}</version>`
 `<configuration>`
 `<systemPropertyVariables>`
 `<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>`
 `<maven.home>${maven.home}</maven.home>`
 `</systemPropertyVariables>`
 `</configuration>`
`</plugin>`

设置java.util.logging.manager系统属性,以确保测试将使用正确的logmanager,并maven.home确保${maven.home}/conf/settings.xml应用了from的自定义配置(如果有)。

该项目还应该包含一个简单的测试:

`package org.acme.getting.started.testing;`
`import io.quarkus.test.junit.QuarkusTest;`
`import org.junit.jupiter.api.Test;`
`import java.util.UUID;`
`import static io.restassured.RestAssured.given;`
`import static org.hamcrest.CoreMatchers.is;`
`@QuarkusTest`
`public class GreetingResourceTest {`
 `@Test`
 `public void testHelloEndpoint() {`
 `given()`
 `.when().get("/hello")`
 `.then()`
 `.statusCode(200)`
 `.body(is("hello"));`
 `}`
 `@Test`
 `public void testGreetingEndpoint() {`
 `String uuid = UUID.randomUUID().toString();`
 `given()`
 `.pathParam("name", uuid)`
 `.when().get("/hello/greeting/{name}")`
 `.then()`
 `.statusCode(200)`
 `.body(is("hello " + uuid));`
 `}`
`}`

该测试使用HTTP直接测试我们的REST端点。运行测试时,将在运行测试之前启动应用程序。

1.1、控制测试端口

尽管 Quarkus 默认会监听 8080 端口,但运行测试时默认为 8081 。 这使您可以在运行应用程序时运行测试。

可以通过在您的quarkus.http.test-port服务器中配置HTTP和
quarkus.http.test-ssl-portHTTPS来配置测试使用的端口application.properties:

`quarkus.http.test-port=8083`
`quarkus.http.test-ssl-port=8446`

Quarkus还提供了 RestAssured 集成,会在运行测试之前配置 RestAssured 使用的默认端口,因此不需要其他配置。

1.2、控制HTTP超时

在测试中使用“ REST安全”时,连接和响应超时设置为30秒。可以使用quarkus.http.test-timeout属性覆盖此设置:

quarkus.http.test-timeout=10s

1.3、注入URI

也有可能直接将URL注入测试中,从而可以轻松使用其他客户端。这是通过@TestHTTPResource注释完成的。

写一个简单的测试,展示一下如何加载一些静态资源。首先在中创建一个简单的HTML文件
src/main/resources/META-INF/resources/index.html:

`<html>`
 `<head>`
 `<title>Testing Guide</title>`
 `</head>`
 `<body>`
 `Information about testing`
 `</body>`
`</html>`

创建一个简单的测试,以确保正确提供该测试:

`package org.acme.getting.started.testing;`
`import java.io.ByteArrayOutputStream;`
`import java.io.IOException;`
`import java.io.InputStream;`
`import java.net.URL;`
`import java.nio.charset.StandardCharsets;`
`import org.junit.jupiter.api.Assertions;`
`import org.junit.jupiter.api.Test;`
`import io.quarkus.test.common.http.TestHTTPResource;`
`import io.quarkus.test.junit.QuarkusTest;`
`@QuarkusTest`
`public class StaticContentTest {`
 `@TestHTTPResource("index.html")` 
 `URL url;//这个注解允许您直接注入Quarkus实例的URL,注解的值将是URL的路径部分`
 `@Test`
 `public void testIndexHtml() throws Exception {`
 `try (InputStream in = url.openStream()) {`
 `String contents = readStream(in);`
 `Assertions.assertTrue(contents.contains("<title>Testing Guide</title>"));`
 `}`
 `}`
 `private static String readStream(InputStream in) throws IOException {`
 `byte[] data = new byte[1024];`
 `int r;`
 `ByteArrayOutputStream out = new ByteArrayOutputStream();`
 `while ((r = in.read(data)) > 0) {`
 `out.write(data, 0, r);`
 `}`
 `return new String(out.toByteArray(), StandardCharsets.UTF_8);`
 `}`
`}`

现在@TestHTTPResource,您可以注入URI,URL和StringURL的表示。

2、测试特定的端点

RESTassured和@TestHTTPResource都允许您指定要测试的端点类,而不是硬编码路径。这目前支持JAX-RS端点、servlet和反应式路由。这使得更容易准确地看到给定测试正在测试的端点。

出于这些示例的目的,假设有一个如下所示的端点:

`@Path("/hello")`
`public class GreetingResource {`
 `@GET`
 `@Produces(MediaType.TEXT_PLAIN)`
 `public String hello() {`
 `return "hello";`
 `}`
`}`

当前,不支持@ApplicationPath()用于设置JAX-RS上下文路径的注释。quarkus.resteasy.path如果要自定义上下文路径,请改用 config值。

2.1、TestHTTPResource

可以使用
io.quarkus.test.common.http.TestHTTPEndpoint注解指定端点路径,然后将从提供的端点中提取路径。如果您还为TestHTTPResource端点指定一个值,它将被附加到端点路径的末尾。

`package org.acme.getting.started.testing;`
`import java.io.ByteArrayOutputStream;`
`import java.io.IOException;`
`import java.io.InputStream;`
`import java.net.URL;`
`import java.nio.charset.StandardCharsets;`
`import org.junit.jupiter.api.Assertions;`
`import org.junit.jupiter.api.Test;`
`import io.quarkus.test.common.http.TestHTTPEndpoint;`
`import io.quarkus.test.common.http.TestHTTPResource;`
`import io.quarkus.test.junit.QuarkusTest;`
`@QuarkusTest`
`public class StaticContentTest {`
 `@TestHTTPEndpoint(GreetingResource.class)` 
 `@TestHTTPResource`
 `URL url;`
 `@Test`
 `public void testIndexHtml() throws Exception {`
 `try (InputStream in = url.openStream()) {`
 `String contents = readStream(in);`
 `Assertions.assertTrue(contents.equals("hello"));`
 `}`
 `}`
 `private static String readStream(InputStream in) throws IOException {`
 `byte[] data = new byte[1024];`
 `int r;`
 `ByteArrayOutputStream out = new ByteArrayOutputStream();`
 `while ((r = in.read(data)) > 0) {`
 `out.write(data, 0, r);`
 `}`
 `return new String(out.toByteArray(), StandardCharsets.UTF_8);`
 `}`
`}`

因为GreetingResource使用注释,@Path("/hello")所以注入的URL将以结尾/hello。

2.2、RESTassured

要控制RESTassured基本路径(即,作为每个请求的根的默认路径),可以使用
io.quarkus.test.common.http.TestHTTPEndpoint注释。这可以在类或方法级别上应用。要测试问候资源,我们将执行以下操作:

`package org.acme.getting.started.testing;`
`import io.quarkus.test.junit.QuarkusTest;`
`import io.quarkus.test.common.http.TestHTTPEndpoint;`
`import org.junit.jupiter.api.Test;`
`import java.util.UUID;`
`import static io.restassured.RestAssured.when;`
`import static org.hamcrest.CoreMatchers.is;`
`@QuarkusTest`
`@TestHTTPEndpoint(GreetingResource.class) //这告诉RESTAssured给所有请求加上前缀/hello。`
`public class GreetingResourceTest {`
 `@Test`
 `public void testHelloEndpoint() {`
 `when().get()    //请注意,无需在此处指定路径,/hello是此测试的默认路径`
 `.then()`
 `.statusCode(200)`
 `.body(is("hello"));`
 `}`
`}`

3、注入测试

Quarkus 可以通过 @Inject 注解将 CDI bean 注入到测试中 (事实上,Quarkus 中的测试是完整的 CDI bean,因此您可以使用所有 CDI 功能)。创建一个简单的测试,不使用 HTTP 直接测试接口:

`package org.acme.getting.started.testing;`
`import javax.inject.Inject;`
`import org.junit.jupiter.api.Assertions;`
`import org.junit.jupiter.api.Test;`
`import io.quarkus.test.junit.QuarkusTest;`
`@QuarkusTest`
`public class GreetingServiceTest {`
 `@Inject //该GreetingServicebean将被注入到测试`
 `GreetingService service;`
 `@Test`
 `public void testGreetingService() {`
 `Assertions.assertEquals("hello Quarkus", service.greeting("Quarkus"));`
 `}`
`}`

4、将拦截器应用于测试

如上所述,Quarkus 测试实际上是完整的 CDI bean, 因此可以像平常一样应用 CDI 拦截器。 比如,如果希望测试方法在事务的上下文中运行,则可以简单地在方法加上注解 @Transactional ,事务拦截器将对其进行处理。

除此之外,还可以创建自己的测试原型 (stereotypes)。 例如,可以创建 @TransactionalQuarkusTest 如下:

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

如果随后将此注释应用于测试类,则其作用就像我们同时应用了@QuarkusTest和 @Transactional注释一样,例如:

`@TransactionalQuarkusTest`
`public class TestStereotypeTestCase {`
 `@Inject`
 `UserTransaction userTransaction;`
 `@Test`
 `public void testUserTransaction() throws Exception {`
 `Assertions.assertEquals(Status.STATUS_ACTIVE, userTransaction.getStatus());`
 `}`
`}`

5、测试和事务

可以在测试上使用标准的Quarkus@Transactional注释,但这意味着测试对数据库所做的更改将是持久的。如果希望在测试结束时回滚所做的任何更改,可以使用
io.quarkus.test.TestTransaction 注释。这将在事务中运行测试方法,但在测试方法完成后将其回滚以还原任何数据库更改。

6、丰富测试回调

作为拦截器的替代或补充,可以通过实现以下回调接口来丰富所有@QuarkusTest类:

  • io.quarkus.test.junit.callback.QuarkusTestBeforeClassCallback

  • io.quarkus.test.junit.callback.QuarkusTestAfterConstructCallback

  • io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback

  • io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback

io.quarkus.test.junit.callback.QuarkusTestBeforeAllCallback为了支持
io.quarkus.test.junit.callback.QuarkusTestAfterConstructCallbackQuarkus而弃用了该版本,并将在Quarkus的将来版本中将其删除

此类回调实现必须注册为所定义的“服务提供者” java.util.ServiceLoader

例如以下示例回调:

`package org.acme.getting.started.testing;`
`import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback;`
`import io.quarkus.test.junit.callback.QuarkusTestMethodContext;`
`public class MyQuarkusTestBeforeEachCallback implements QuarkusTestBeforeEachCallback {`
 `@Override`
 `public void beforeEach(QuarkusTestMethodContext context) {`
 `System.out.println("Executing " + context.getTestMethod());`
 `}`
`}`

必须通过
src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback以下方式进行注册:

org.acme.getting.started.testing.MyQuarkusTestBeforeEachCallback
  • 可以从测试类或方法中读取注释,以控制回调应执行的操作。
  • 尽管可以使用像这样的JUnit Jupiter回调接口BeforeEachCallback,但由于Quarkus必须在JUnit不知道的自定义类加载器中运行测试,因此您可能会遇到类加载问题

7、测试不同Profiles

到目前为止,在所有示例中,对于所有测试,仅一次启动Quarkus。在运行第一个测试之前,Quarkus将启动,然后所有测试都将运行,然后Quarkus将在最后关闭。这提供了非常快速的测试体验,但是由于无法测试不同的配置而受到了一定的限制。

为了解决这个问题,Quarkus支持测试配置文件的想法。如果测试的配置文件与之前运行的测试文件不同,则在运行测试之前,将关闭Quarkus并使用新的配置文件启动。这显然要慢一些,因为它增加了测试时间的关闭/启动周期,但具有很大的灵活性。

  • 为了减少Quarkus需要重启的次数,建议您将所有需要特定配置文件的测试放入它们自己的包中,然后按字母顺序运行测试。

7.1、 编辑一个Profile

要实现测试配置文件,需要实现
io.quarkus.test.junit.QuarkusTestProfile:

`package org.acme.getting.started.testing;`
`import java.util.Collections;`
`import java.util.List;`
`import java.util.Map;`
`import java.util.Set;`
`import io.quarkus.test.junit.QuarkusTestProfile;`
`import io.quarkus.test.junit.QuarkusTestProfile.TestResourceEntry;`
`public class MockGreetingProfile implements QuarkusTestProfile {`
 `@Override`
 `public Map<String, String> getConfigOverrides() {` 
 `//这种方法使可以覆盖配置属性。在这里,正在更改JAX-RS根路径。`
 `return Collections.singletonMap("quarkus.resteasy.path","/api");`
 `}`
 `@Override`
 `public Set<Class<?>> getEnabledAlternatives() {` 
 `//这种方法能够启用CDI @Alternativebean。这样可以轻松模拟出某些bean功能。`
 `return Collections.singleton(MockGreetingService.class);`
 `}`
 `@Override`
 `//这可用于更改配置文件。由于此默认设置test不执行任何操作,因此为了完整性起见将其包括在内。`
 `public String getConfigProfile() {` 
 `return "test";`
 `}`
 `@Override`
 `public List<TestResourceEntry> testResources() {` 
 `//此方法使我们可以应用仅适用于此配置文件的其他 QuarkusTestResourceLifecycleManager类。如果不重写此方法,`
 `//则仅使用QuarkusTestResourceLifecycleManager通过@QuarkusTestResource类注释启用的类将用于使用此配置文件的测试`
 `//(与完全不使用配置文件的测试的行为相同)。`
 `return Collections.singletonList(new TestResourceEntry(CustomWireMockServerManager.class));`
 `}`
`}`

现在,已经定义了配置文件,需要将其包含在测试类中。使用@TestProfile(MockGreetingProfile.class)。

所有的测试配置文件配置都存储在一个类中,这使很容易分辨以前的测试是否以相同的配置运行。

7.2、运行特定的测试

Quarkus提供了将测试执行限制为带有特定@TestProfile注释的测试的功能 。通过利用与系统属性结合的tags方法来QuarkusTestProfile工作quarkus.test.profile.tags。

本质上,任何QuarkusTestProfile具有至少一个与值匹配的匹配标签的变量都quarkus.test.profile.tags将被认为是活动的,并且所有带有@TestProfile活动配置文件注释的测试都将运行,而其余测试将被跳过。在下面的示例中最好地显示了这一点。

首先,定义一些这样的QuarkusTestProfile实现:

`public class Profiles {`
 `public static class NoTags implements QuarkusTestProfile {`
 `}`
 `public static class SingleTag implements QuarkusTestProfile {`
 `@Override`
 `public Set<String> tags() {`
 `return Collections.singleton("test1");`
 `}`
 `}`
 `public static class MultipleTags implements QuarkusTestProfile {`
 `@Override`
 `public Set<String> tags() {`
 `return new HashSet<>(Arrays.asList("test1", "test2"));`
 `}`
 `}`
`}`

现在,假设我们有以下测试:

`@QuarkusTest`
`public class NoQuarkusProfileTest {`
 `@Test`
 `public void test() {`
 `// test something`
 `}`
`}`

@QuarkusTest @TestProfile(Profiles.NoTags.class) public class NoTagsTest { @Test public void test() { // test something } }

`@QuarkusTest`
`@TestProfile(Profiles.SingleTag.class)`
`public class SingleTagTest {`
 `@Test`
 `public void test() {`
 `// test something`
 `}`
`}`

@QuarkusTest @TestProfile(Profiles.MultipleTags.class) public class MultipleTagsTest { @Test public void test() { // test something } }


考虑以下情形:

*   quarkus.test.profile.tags 未设置:将执行所有测试。
*   quarkus.test.profile.tags=foo:在这种情况下,将不会执行任何测试,因为在QuarkusTestProfile实现上定义的所有标签均不匹配的值quarkus.test.profile.tags。请注意,NoQuarkusProfileTest由于未使用注释,因此也不执行@TestProfile。
*   quarkus.test.profile.tags=test1:在这种情况下SingleTagTest,MultipleTagsTest将运行,因为它们各自QuarkusTestProfile实现上的标记与的值匹配quarkus.test.profile.tags。
*   quarkus.test.profile.tags=test1,test3:这种情况导致执行与前一种情况相同的测试。
*   quarkus.test.profile.tags=test2,test3:在这种情况下,只有MultipleTagsTest将运行,因为MultipleTagsTest是唯一的QuarkusTestProfile实现,其tags方法的价值相匹配quarkus.test.profile.tags。

8、模拟(Mock)支持
============

Quarkus支持使用两种不同的方法来使用模拟对象。可以使用CDI替代品为所有测试类模拟出一个bean,也可以QuarkusMock基于每个测试来模拟出一个bean 。

8.1、CDI@Alternative机制。
======================

要使用它,只需在src/test/java目录中用类重写要模拟的bean,并在bean上添加@Alternative和@Priority(1)注释。或者,一个方便的io.quarkus.test.Mock 可以使用原型注释。这个内置的原型声明@Alternative、@Priority(1)和@Dependent。例如,有以下服务:

@ApplicationScoped public class ExternalService { public String service() { return "external"; } }


可以在下面的类中模拟它src/test/java:

@Mock @ApplicationScoped //覆盖在@Dependent构造型上声明的范围@Mock。 public class MockExternalService extends ExternalService { @Override public String service() { return "mock"; } }


重要的是,替代项应出现在src/test/java目录中而不是中src/main/java,因为否则替代项将一直有效,而不仅是在测试时。

请注意,目前这种方法不适用于本机图像测试,因为这将需要将测试替代方法烘焙到本机图像中。

8.2、使用QuarkusMock模拟
===================

  
io.quarkus.test.junit.QuarkusMock类可用于暂时模拟出任何正常范围的Bean。如果在方法中使用此方法,则@BeforeAll该模拟将对当前类的所有测试生效,而如果在测试方法中使用此方法,则该模拟将仅在当前测试期间生效。

此方法可用于任何正常范围的CDI bean(例如@ApplicationScoped@RequestScoped等等,基本上除了@Singleton和之外的每个范围@Dependent)。

用法示例如下所示:

@QuarkusTest public class MockTestCase { @Inject MockableBean1 mockableBean1; @Inject MockableBean2 mockableBean2; @BeforeAll public static void setup() { MockableBean1 mock = Mockito.mock(MockableBean1.class); Mockito.when(mock.greet("Stuart")).thenReturn("A mock for Stuart"); QuarkusMock.installMockForType(mock, MockableBean1.class); //由于此处无法使用注入的实例installMockForType,因此该模拟方法可用于两种测试方法 } @Test public void testBeforeAll() { Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart")); Assertions.assertEquals("Hello Stuart", mockableBean2.greet("Stuart")); } @Test public void testPerTestMock() { //installMockForInstance用来替换注入的bean,这在测试方法期间有效。 QuarkusMock.installMockForInstance(new BonjourGreeter(), mockableBean2); Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart")); Assertions.assertEquals("Bonjour Stuart", mockableBean2.greet("Stuart")); } @ApplicationScoped public static class MockableBean1 { public String greet(String name) { return "Hello " + name; } } @ApplicationScoped public static class MockableBean2 { public String greet(String name) { return "Hello " + name; } } public static class BonjourGreeter extends MockableBean2 { @Override public String greet(String name) { return "Bonjour " + name; } } }


请注意,它不依赖Mockito,可以使用任何喜欢的模拟库,甚至可以手动覆盖对象以提供所需的行为。

8.2.1、进一步简化@InjectMock
======================

QuarkusMockQuarkus在提供的功能的基础上,还允许用户毫不费力地利用Mockito来模拟Mockito支持的bean QuarkusMock。可通过依赖项中可用的@  
io.quarkus.test.junit.mockito.InjectMock注释来使用此功能quarkus-junit5-mockito。

使用@InjectMock,上一个示例可以编写如下:

@QuarkusTest public class MockTestCase { @InjectMock MockableBean1 mockableBean1; @InjectMock MockableBean2 mockableBean2; @BeforeEach public void setup() { Mockito.when(mockableBean1.greet("Stuart")).thenReturn("A mock for Stuart"); } @Test public void firstTest() { Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart")); Assertions.assertEquals(null, mockableBean2.greet("Stuart")); } @Test public void secondTest() { Mockito.when(mockableBean2.greet("Stuart")).thenReturn("Bonjour Stuart"); Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart")); Assertions.assertEquals("Bonjour Stuart", mockableBean2.greet("Stuart")); } @ApplicationScoped public static class MockableBean1 { public String greet(String name) { return "Hello " + name; } } @ApplicationScoped public static class MockableBean2 { public String greet(String name) { return "Hello " + name; } } }


*   @InjectMock导致模拟存在和是在测试类的试验方法可用的(其它测试类不影响此)
*   在mockableBean1此为该类的每种测试方法配置
*   由于mockableBean2尚未配置模拟,它将返回默认的Mockito响应。
*   在此测试中,mockableBean2已配置,因此它将返回已配置的响应。

尽管上面的测试很好地展示了的功能@InjectMock,但它并不是真实测试的良好表示。在真实的测试中,很可能会配置一个模拟,但是然后测试一个使用模拟的bean的bean。这是一个例子:

@QuarkusTest public class MockGreetingServiceTest { @InjectMock GreetingService greetingService; @Test public void testGreeting() { when(greetingService.greet()).thenReturn("hi"); given() .when().get("/greeting") .then() .statusCode(200) .body(is("hi")); //由于将其配置greetingService为GreetingResource使用GreetingServicebean的模拟, //因此得到了模拟响应,而不是常规GreetingServicebean的响应。 } @Path("greeting") public static class GreetingResource { final GreetingService greetingService; public GreetingResource(GreetingService greetingService) { this.greetingService = greetingService; } @GET @Produces("text/plain") public String greet() { return greetingService.greet(); } } @ApplicationScoped public static class GreetingService { public String greet(){ return "hello"; } } }


8.2.2、在@InjectSpy中使用Spies而不是mock
================================

在InjectMockQuarkus提供的功能的基础上,Quarkus还允许用户毫不费力地利用Mockito来监视由Mockito支持的bean QuarkusMock。可通过依赖项中可用的@  
io.quarkus.test.junit.mockito.InjectSpy注释来使用此功能quarkus-junit5-mockito。

有时,在测试时,只需要验证是否采用了某个逻辑路径,或者只需要在执行Spied克隆上的其余方法的同时对单个方法的响应进行存根即可。请参阅Mockito文档以获取有关Spy部分模拟的更多详细信息。在这两种情况下,最好都使用间谍对象。使用@InjectSpy,上一个示例可以编写如下:

@QuarkusTest public class SpyGreetingServiceTest { @InjectSpy GreetingService greetingService; @Test public void testDefaultGreeting() { given() .when().get("/greeting") .then() .statusCode(200) .body(is("hello")); Mockito.verify(greetingService, Mockito.times(1)).greet(); //只是要确保GreetingService此测试调用了对我们的greet方法,而不是覆盖该值。 } @Test public void testOverrideGreeting() { when(greetingService.greet()).thenReturn("hi"); //告诉间谍返回“ hi”而不是“ hello”。当GreetingResource请求来自GreetingService的问候时, //得到的是模拟响应,而不是常规GreetingServiceBean的响应。 given() .when().get("/greeting") .then() .statusCode(200) .body(is("hi")); //正在验证是否从间谍获得了模拟的响应。 } @Path("greeting") public static class GreetingResource { final GreetingService greetingService; public GreetingResource(GreetingService greetingService) { this.greetingService = greetingService; } @GET @Produces("text/plain") public String greet() { return greetingService.greet(); } } @ApplicationScoped public static class GreetingService { public String greet(){ return "hello"; } } }


8.2.3、使用@InjectMock@RestClient
===============================

这些@RegisterRestClient寄存器在运行时注册rest-client的实现,并且由于Bean需要是常规作用域,因此必须使用注释接口@ApplicationScoped

@Path("/") @ApplicationScoped @RegisterRestClient public interface GreetingService { @GET @Path("/hello") @Produces(MediaType.TEXT_PLAIN) String hello(); }


对于测试类,这是一个示例:

@QuarkusTest public class GreetingResourceTest { @InjectMock @RestClient //指示此注入点旨在使用的实例RestClient。 GreetingService greetingService; @Test public void testHelloEndpoint() { Mockito.when(greetingService.hello()).thenReturn("hello from mockito"); given() .when().get("/hello") .then() .statusCode(200) .body(is("hello from mockito")); } }


9、在Quarkus应用程序启动之前启动服务

一个非常普遍的需求是在Quarkus应用程序开始进行测试之前,启动Quarkus应用程序所依赖的某些服务。为了满足这一需求,Quarkus提供了@  
io.quarkus.test.common.QuarkusTestResource和  
io.quarkus.test.common.QuarkusTestResourceLifecycleManager。

通过简单地用注释测试套件中的任何测试@QuarkusTestResource,Quarkus将在运行  
QuarkusTestResourceLifecycleManager任何测试之前运行相应的测试。测试套件还可以自由使用多个@QuarkusTestResource批注,在这种情况下,所有相应的  
QuarkusTestResourceLifecycleManager对象都将在测试之前运行。当使用多个测试资源时,它们可以同时启动。为此,您需要设置@QuarkusTestResource(parallel = true)。

测试资源是全局的,即使它们是在测试类或自定义配置文件上定义的也是如此,这意味着即使我们确实删除了重复项,所有测试都将激活它们。如果只想在单个测试类或测试配置文件上启用测试资源,则可以使用@QuarkusTestResource(restrictToAnnotatedClass = true)。

Quarkus提供了一些现成的实现  
QuarkusTestResourceLifecycleManager(请参阅  
io.quarkus.test.h2.H2DatabaseTestResource哪个启动了H2数据库,或者  
io.quarkus.test.kubernetes.client.KubernetesMockServerTestResource哪个启动了模拟的Kubernetes API服务器),但是创建自定义实现来满足特定的应用程序需求是很常见的。常见情况包括使用Testcontainers启动docker容器(可在此处找到示例),或使用Wiremock启动模拟HTTP服务器(可在此处找到示例)。

9.1、基于注释的测试资源
=============

可以编写使用注释启用和配置的测试资源。可以通过@QuarkusTestResource 在注释上放置来启用该注释,该注释将用于启用和配置测试资源。

例如,这定义了@WithKubernetesTestServer注释,您可以在测试中使用该注释来激活  
KubernetesServerTestResource,但仅用于带注释的测试类。您也可以将它们放在QuarkusTestProfile测试配置文件中。

@QuarkusTestResource(KubernetesServerTestResource.class) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface WithKubernetesTestServer { /** * Start it with HTTPS */ boolean https() default false; /** * Start it in CRUD mode */ boolean crud() default true; /** * Port to use, defaults to any available port */ int port() default 0; }


本  
KubernetesServerTestResource类必须实现  
QuarkusTestResourceConfigurableLifecycleManager以使用先前的注解配置界面:

public class KubernetesServerTestResource implements QuarkusTestResourceConfigurableLifecycleManager<WithKubernetesTestServer> { private boolean https = false; private boolean crud = true; private int port = 0; @Override public void init(WithKubernetesTestServer annotation) { this.https = annotation.https(); this.crud = annotation.crud(); this.port = annotation.port(); } // ... }


10、挂起检测
=======

@QuarkusTest支持挂起检测以帮助诊断任何意外的挂起。如果在指定时间内未取得任何进展(即未调用JUnit回调),则Quarkus将向控制台打印堆栈跟踪以帮助诊断挂起。此超时的默认值为10分钟。

将不会采取进一步的措施,并且测试将继续正常进行(通常直到CI超时为止),但是打印出的堆栈跟踪信息应有助于诊断构建失败的原因。您可以使用  
quarkus.test.hang-detection-timeout系统属性来控制此超时 (您也可以在application.properties中进行设置,但是直到Quarkus启动后才会读取该超时,因此Quarkus启动的超时将默认为10分钟)。

11、本机可执行测试
==========

也可以使用来测试本机可执行文件@NativeImageTest。它支持本指南中提到的所有功能,除了注入测试外(本机可执行文件在单独的非JVM进程中运行,这实际上是不可能的)。

12、使用@QuarkusIntegrationTest
============================

@QuarkusIntegrationTest应该用于启动和测试Quarkus构建产生的工件,并支持测试jar(任何类型),本机映像或容器映像。简而言之,这意味着如果Quarkus构建(mvn package或gradle build)的结果是一个jar,该jar将作为启动,java -jar …并对其进行测试。相反,如果构建了本机映像,则将在启动应用程序时./application …​再次针对正在运行的应用程序运行测试。最后,如果在构建过程中创建了容器映像(通过使用包括  
quarkus-container-image-jib或  
quarkus-container-image-docker扩展名并  
quarkus.container-image.build=true配置了 属性),则将创建并运行容器(这需要存在docker可执行文件)。

与@NativeImageTest一样,这是一个黑盒测试,它支持相同的设置功能并具有相同的限制。

![云原生-Quarkus测试应用程序](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a9c562fb9f054962bad96ee3e548af20~tplv-k3u1fbpfcp-zoom-1.image)