云原生-Quarkus测试应用程序

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,您可以注入URIURLStringURL的表示。

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:在这种情况下SingleTagTestMultipleTagsTest将运行,因为它们各自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.QuarkusTestResourceio.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 packagegradle build)的结果是一个jar,该jar将作为启动,java -jar …并对其进行测试。相反,如果构建了本机映像,则将在启动应用程序时./application …再次针对正在运行的应用程序运行测试。最后,如果在构建过程中创建了容器映像(通过使用包括quarkus-container-image-jibquarkus-container-image-docker扩展名并quarkus.container-image.build=true配置了 属性),则将创建并运行容器(这需要存在docker可执行文件)。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容