从其测试代码中覆盖 Quarkus 应用程序的配置
为了实现良好的测试覆盖率,通常需要从测试代码中覆盖 Quarkus 应用的配置。每当一个配置属性决定应用程序的行为方式时,都需要测试所有可能的配置值。
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class MyService {
@Inject
MyConfig config;
public void doSomething() {
if (config.newFeatureEnabled()) {
// This branch needs to be tested.
} else {
// So does that branch.
}
}
}
@ConfigMapping(prefix = "my-config")
interface MyConfig { (1)
@WithDefault("false")
boolean newFeatureEnabled();
}
1 | 在实际项目中,此接口很可能为 public 并在单独的文件中声明。 |
有很多方法可以从测试代码中覆盖配置。本文将向您展示五种方法,并重点介绍每种方法的优点和缺点。
本文中的所有代码片段(以及更多!)都可以在 gwenneg/blog-overriding-configuration-from-test-code 存储库中找到。 |
方法 #1:Quarkus 测试配置文件
Quarkus 测试配置文件是覆盖配置的最佳方法之一。它们可以在本机模式下进行测试时使用,而本文中的大多数方法则不能。除了配置覆盖之外,它们还提供了许多其他功能,可以更轻松地测试 Quarkus 应用。
从配置覆盖的角度来看,测试配置文件确实存在一些缺点。首先,Quarkus 在使用每个测试配置文件之前都会重新启动,这显然会减慢测试执行速度。为了覆盖同一配置属性的多个值,测试还必须分为多个测试配置文件和类。因此,更大的项目可能会最终拥有大量测试配置文件,并在测试之间花费大量时间重新启动 Quarkus。使用测试配置文件维护或审查测试代码也可能更具挑战性。
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@Path("/features")
public class FeaturesResource {
@Inject
FeaturesConfig featuresConfig; (1)
@ConfigProperty(name = "amazing-feature-enabled", defaultValue = "false") (1)
boolean amazingFeatureEnabled;
@GET
@Path("/awesome")
public boolean isAwesomeFeatureEnabled() {
return featuresConfig.awesomeFeatureEnabled();
}
@GET
@Path("/amazing")
public boolean isAmazingFeatureEnabled() {
return amazingFeatureEnabled;
}
}
@ConfigMapping(prefix = "features")
interface FeaturesConfig { (2)
@WithDefault("false")
boolean awesomeFeatureEnabled();
}
1 | 测试配置文件既支持配置映射,也支持 @ConfigProperty 。 |
2 | 在实际项目中,此接口很可能为 public 并在单独的文件中声明。 |
大多数关于测试配置文件的指南都会以冗长的方式介绍它们,以展示它们的所有功能。实际上,只需添加几行额外代码,就可以将测试配置文件添加到现有的测试类中
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
import io.restassured.RestAssured;
import java.util.Map;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.Test;
@QuarkusTest
@TestProfile(FeaturesResourceTest.class)
public class FeaturesResourceTest implements QuarkusTestProfile { (1)
@Override
public Map<String, String> getConfigOverrides() { (2)
return Map.of(
"features.awesome-feature-enabled", "true", (3)
"amazing-feature-enabled", "true"
);
}
@Test
void test() {
RestAssured.given()
.when().get("/features/awesome")
.then().body(CoreMatchers.is("true"));
RestAssured.given()
.when().get("/features/amazing")
.then().body(CoreMatchers.is("true"));
}
}
1 | 如果配置文件不与多个测试类共享,测试类本身可以实现 QuarkusTestProfile 。这可以使测试代码的维护和审查更容易。如果多个测试类依赖于同一配置文件,那么该配置文件很可能需要在专用类中声明。 |
2 | 此方法来自 QuarkusTestProfile ,可以从测试代码中覆盖配置。 |
3 | 从 FeaturesConfig 接口生成的配置键以 features. 为前缀,而来自 @ConfigProperty 注入的配置键没有前缀。 |
测试配置文件还可以利用配置文件感知文件从测试代码中覆盖配置
features.awesome-feature-enabled=true
使用时,测试配置文件需要覆盖默认配置配置文件
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
import io.restassured.RestAssured;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.Test;
@QuarkusTest
@TestProfile(FeaturesResourceTest.class)
public class FeaturesResourceTest implements QuarkusTestProfile {
@Override
public String getConfigProfile() { (1)
return "blog"; (2)
}
@Test
void test() {
RestAssured.given()
.when().get("/features/awesome")
.then().body(CoreMatchers.is("true"));
}
}
1 | 此方法来自 QuarkusTestProfile ,可以覆盖默认配置配置文件。 |
2 | application-blog.properties 文件将被加载,因为 blog 配置配置文件是活动的。 |
如果测试仅在 JVM 模式下运行而不是在本机模式下运行,则 application-blog.properties
文件可以放在 src/test/resources
文件夹中。还需要在同一位置放置一个额外的 application.properties
文件(可能为空)以启用配置文件感知文件。
如果测试在本机模式下运行,也需要相同的 application-blog.properties
和 application.properties
文件,但它们必须放在 src/main/resources
文件夹中。application.properties
文件还需要包含以下行
quarkus.native.resources.includes=application*.properties
方法 #2:使用 Mockito 模拟配置
现在,这是我不要求本机测试时的首选方法。
首先,让我们看看它如何与配置映射配合使用
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path("/features")
public class FeaturesResource {
@Inject
FeaturesConfig featuresConfig;
@GET
@Path("/awesome")
public boolean isAwesomeFeatureEnabled() {
return featuresConfig.awesomeFeatureEnabled();
}
}
@ConfigMapping(prefix = "features")
interface FeaturesConfig { (1)
@WithDefault("false")
boolean awesomeFeatureEnabled();
}
1 | 在实际项目中,此接口很可能为 public 并在单独的文件中声明。 |
import io.quarkus.test.InjectMock;
import io.quarkus.test.Mock;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.smallrye.config.SmallRyeConfig;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.Config;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusTest
public class FeaturesResourceTest {
@Inject
SmallRyeConfig smallRyeConfig;
@Produces (1)
@ApplicationScoped
@Mock
FeaturesConfig featuresConfig() { (2)
return smallRyeConfig.getConfigMapping(FeaturesConfig.class);
}
@InjectMock (3)
FeaturesConfig featuresConfig;
@Test
void test() {
Mockito.when(featuresConfig.awesomeFeatureEnabled()).thenReturn(true); (4)
RestAssured.given()
.when().get("/features/awesome")
.then().body(CoreMatchers.is("true"));
}
}
1 | 可以省略此注解。 |
2 | 这是使 FeaturesConfig 接口实现可代理所必需的。没有它,就无法使用 @InjectMock 来模拟它。 |
3 | 借助 quarkus-junit5-mockito 扩展,可以模拟配置类。本机模式下的测试不支持注入,因此这仅在 JVM 模式下运行测试时才有效。 |
4 | 配置可以从测试方法或使用 JUnit 的生命周期注解(如 @BeforeEach )进行注解的方法进行模拟。 |
如果您的项目依赖于 @ConfigProperty
而不是 @ConfigMapping
怎么办?这也可以!您只需将配置属性移到一个额外的 @ApplicationScoped
bean 中。该 bean 可能用于集中 Quarkus 应用的所有配置属性,也可能不用于。
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.event.Startup;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@ApplicationScoped
public class FeaturesConfig {
private static final String AWESOME_FEATURE_ENABLED = "awesome-feature-enabled";
@ConfigProperty(name = AWESOME_FEATURE_ENABLED, defaultValue = "false")
boolean awesomeFeatureEnabled;
// Omitted: additional config properties.
public boolean isAwesomeFeatureEnabled() {
return awesomeFeatureEnabled;
}
// This is an optional bonus unrelated to the blog post topic.
void logConfigAtStartup(@Observes Startup event) { (1)
Map<String, Object> config = new TreeMap<>(); (2)
config.put(AWESOME_FEATURE_ENABLED, awesomeFeatureEnabled);
// Omitted: put all config keys and values into the map.
Log.info("=== Startup configuration ===");
config.forEach((key, value) -> {
Log.infof("%s=%s", key, value); (3)
});
}
}
1 | 此方法在应用程序启动时执行。有关应用程序生命周期事件的更多详细信息,请参阅应用程序初始化和终止指南。 |
2 | TreeMap 有助于按键字母顺序自动排序 Map 条目。 |
3 | 应用程序配置在启动时进行日志记录。如果您需要根据过去的日志来调查问题,这确实很有帮助。但是,请注意不要记录任何敏感的配置值!(例如,秘密或密码) |
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path("/features")
public class FeaturesResource {
@Inject
FeaturesConfig featuresConfig;
@GET
@Path("/awesome")
public boolean isAwesomeFeatureEnabled() {
return featuresConfig.isAwesomeFeatureEnabled();
}
}
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusTest
public class FeaturesResourceTest {
@InjectMock (1)
FeaturesConfig featuresConfig;
@Test
void test() {
Mockito.when(featuresConfig.isAwesomeFeatureEnabled()).thenReturn(true); (2)
RestAssured.given()
.when().get("/features/awesome")
.then().body(CoreMatchers.is("true"));
}
}
1 | 借助 quarkus-junit5-mockito 扩展,可以模拟配置类。本机模式下的测试不支持注入,因此这仅在 JVM 模式下运行测试时才有效。 |
2 | 配置可以从测试方法或使用 JUnit 的生命周期注解(如 @BeforeEach )进行注解的方法进行模拟。 |
此方法还可以利用 JUnit 的 @ParameterizedTest
功能,并使用单个测试方法测试配置属性的多个值
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
@QuarkusTest
public class FeaturesResourceTest {
@InjectMock
FeaturesConfig featuresConfig;
@ParameterizedTest
@ValueSource(booleans = {true, false})
void test(boolean awesomeFeatureEnabled) { (1)
Mockito.when(featuresConfig.isAwesomeFeatureEnabled()).thenReturn(awesomeFeatureEnabled);
RestAssured.given()
.when().get("/features/awesome")
.then().body(CoreMatchers.is(String.valueOf(awesomeFeatureEnabled)));
}
}
1 | 运行测试时,此方法将针对 @ValueSource 注解提供的每个值调用一次。 |
方法 #3:构造函数注入
如果您需要在大型项目中进行本机测试,而该项目存在本文前面提到的 Quarkus 测试配置文件的缺点?通过 CDI bean 的构造函数注入可能是适合您的方法。
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
import jakarta.inject.Singleton;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@Singleton
public class FeaturesService {
private final FeaturesConfig featuresConfig;
private final boolean amazingFeatureEnabled;
public FeaturesService( (1)
FeaturesConfig featuresConfig,
@ConfigProperty(name = "amazing-feature-enabled", defaultValue = "false") boolean amazingFeatureEnabled
) {
this.featuresConfig = featuresConfig;
this.amazingFeatureEnabled = amazingFeatureEnabled;
}
public boolean isAwesomeFeatureEnabled() {
return featuresConfig.awesomeFeatureEnabled();
}
public boolean isAmazingFeatureEnabled() {
return amazingFeatureEnabled;
}
}
@ConfigMapping(prefix = "features")
interface FeaturesConfig { (2)
@WithDefault("false")
boolean awesomeFeatureEnabled();
}
1 | 配置被注入到 CDI bean 的构造函数中。此方法支持配置映射和 @ConfigProperty 。 |
2 | 在实际项目中,此接口很可能为 public 并在单独的文件中声明。 |
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@QuarkusTest
public class FeaturesServiceTest {
@Test
void test() {
FeaturesConfig featuresConfig = new FeaturesConfig() { (1)
@Override
public boolean awesomeFeatureEnabled() {
return true;
}
};
FeaturesService featuresService = new FeaturesService(featuresConfig, true); (2)
Assertions.assertTrue(featuresService.isAwesomeFeatureEnabled());
Assertions.assertTrue(featuresService.isAmazingFeatureEnabled());
}
}
1 | 这用于从 FeaturesConfig 接口覆盖配置。 |
2 | 当 bean 构造函数被调用时,配置会从测试中被覆盖。第一个参数覆盖依赖于 @ConfigMapping 的配置。第二个参数覆盖依赖于 @ConfigProperty 的配置。 |
通过这种方法,在运行测试时 CDI 不会执行任何注入,因为 bean 是手动实例化的,而不是由 Quarkus 的 CDI 容器管理的。可以通过在被测试 bean 的构造函数中注入所有依赖项(其他 bean 和/或配置)来缓解此缺点。完成此操作后,CDI 注入仍然无法正常工作,但测试代码将能够提供测试执行所需的所有依赖项。
方法 #4:测试组件
Quarkus 最近推出了一项实验性功能,称为测试组件,可用于从测试代码中覆盖配置。该功能由 quarkus-junit5-component
扩展提供。
此方法不会启动完整的 Quarkus 应用。它仅启动 CDI 容器并注入用 @jakarta.inject.Inject
或 @io.quarkus.test.InjectMock
注解的测试字段。因此,与Quarkus 测试配置文件中的完整 Quarkus 应用重启相比,它可能更快,尤其是在大型项目中。
此方法不适用于本机测试,因为它依赖于测试代码中的注入,而这些注入仅在 JVM 模式下运行测试时才受支持。
让我们看看它是如何工作的
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@ApplicationScoped
public class FeaturesService {
@Inject
FeaturesConfig featuresConfig; (1)
@ConfigProperty(name = "amazing-feature-enabled", defaultValue = "false") (1)
boolean amazingFeatureEnabled;
public boolean isAwesomeFeatureEnabled() {
return featuresConfig.awesomeFeatureEnabled();
}
public boolean isAmazingFeatureEnabled() {
return amazingFeatureEnabled;
}
}
@ConfigMapping(prefix = "features")
interface FeaturesConfig { (2)
@WithDefault("false")
boolean awesomeFeatureEnabled();
}
1 | 测试组件支持配置映射和 @ConfigProperty 。 |
2 | 在实际项目中,此接口很可能为 public 并在单独的文件中声明。 |
import io.quarkus.test.component.QuarkusComponentTest;
import io.quarkus.test.component.TestConfigProperty;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@QuarkusComponentTest (1)
@TestConfigProperty(key = "features.awesome-feature-enabled", value = "true") (2)
public class FeaturesServiceTest {
@Inject
FeaturesService featuresService;
@Test
@TestConfigProperty(key = "amazing-feature-enabled", value = "true") (2)
void test() {
Assertions.assertTrue(featuresService.isAwesomeFeatureEnabled());
Assertions.assertTrue(featuresService.isAmazingFeatureEnabled());
}
}
1 | 普通的 @QuarkusTest 注解已被 @QuarkusComponentTest 取代。 |
2 | @TestConfigProperty 可以用于测试类、测试方法或两者兼有。 |
方法 #5:系统属性
我绝对不推荐这种方法,但它确实存在并且也能奏效,所以我还是提一下。系统属性可用于从测试代码中覆盖配置。但是,此方法存在重大缺点
-
它在原生模式下不起作用。
-
它不适用于配置映射。
-
当配置定义在
@ApplicationScoped
或@Singleton
bean 中,并在该 bean 初始化之前设置时,它只能生效一次。在 bean 初始化之后,对系统属性所做的任何更改都不会影响配置。
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@Path("/features")
public class FeaturesResource {
@ConfigProperty(name = "awesome-feature-enabled", defaultValue = "false")
boolean awesomeFeatureEnabled;
@GET
@Path("/awesome")
public boolean isAwesomeFeatureEnabled() {
return awesomeFeatureEnabled;
}
}
可以使用 Maven 或 Gradle 从命令行设置系统属性
./mvnw verify -Dawesome-feature-enabled=true
也可以从测试代码中设置它们
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) (1)
class FeaturesResourceTest {
@Test
@Order(1) (2)
void firstTest() {
System.setProperty("awesome-feature-enabled", "true");
RestAssured.given()
.when().get("/features/awesome")
.then().body(CoreMatchers.is("true"));
}
@Test
@Order(2) (3)
void lastTest() {
System.setProperty("awesome-feature-enabled", "false");
RestAssured.given()
.when().get("/features/awesome")
.then().body(CoreMatchers.is("true")); (4)
}
}
1 | 在此代码片段中,测试按固定顺序运行以演示系统属性的局限性。 |
2 | 此测试始终第一个运行。 |
3 | 此测试始终最后一个运行。 |
4 | 此测试依赖于具有默认 @Singleton 作用域的 CDI bean,该 bean 已由前一个测试初始化。因此,此测试的结果无法通过系统属性进行更改。 |
结论
首先,本文并非从测试代码中覆盖配置的所有现有方法的详尽列表。还有其他选项,例如使用反射(难以维护),我没有包含在内,可能还有我甚至不知道的方法。请随时在评论中分享您对这个话题的经验和看法!
你们中的大多数人在阅读本文时可能心里都有一个问题:哪种方法最好?嗯,正如您可能在本文中理解的那样,它们(目前)都不是完美的。它们都有缺点。以我的经验来看,真正的问题不是选择最佳方法,而是如何更好地结合不同的方法并利用它们提供的最佳功能。
如果您不确定要在项目中引入哪种方法,gwenneg/blog-overriding-configuration-from-test-code 存储库可能会帮助您做出决定。它包含本文提到的所有方法的实现。
感谢您阅读本文!希望它能帮助您更好地测试 Quarkus 应用。