1.概述
当它依赖于系统资源(例如环境变量,系统属性)或使用进程级操作(例如System.exit
时,可能很难测试我们的软件。
Java没有提供用于设置环境变量的直接方法,因此我们冒着风险,即在一个测试中设置的值会影响另一个测试的执行。同样,我们可能会避免为可能执行System.exit
代码编写JUnit测试,因为它有可能中止测试。
系统规则和系统Lambda库是这些问题的早期解决方案。在本教程中,我们将研究一个名为System Stubs的System Lambda的新分支,它提供了JUnit 5替代方案。
2.为什么要使用系统存根?
2.1。系统Lambda不是JUnit插件
原始的系统规则库仅可用于JUnit4。在JUnit 5下,它仍可与JUnit Vintage一起使用,但这需要继续创建JUnit 4测试。该库的创建者产生了一个与系统无关的测试框架版本,称为System Lambda,该版本旨在在每种测试方法中使用:
@Test
void aSingleSystemLambda() throws Exception {
restoreSystemProperties(() -> {
System.setProperty("log_dir", "test/resources");
assertEquals("test/resources", System.getProperty("log_dir"));
});
// more test code here
}
测试代码表示为lambda,传递给设置必要的存根的方法。清理将在控制权返回到其余测试方法之前进行。
尽管此方法在某些情况下效果很好,但该方法有一些缺点。
2.2。避免额外的代码
系统Lambda方法的好处是,工厂类中有一些通用的配方可以执行特定类型的测试。但是,当我们想在许多测试用例中使用它时,这会导致一些代码膨胀。
首先,即使测试代码本身未引发检查异常,但wrapper方法也会throws Exception
。其次,在多个测试中设置相同的规则需要代码重复。每个测试都需要独立执行相同的配置。
但是,这种方法最麻烦的方面是当我们尝试一次设置多个工具时。假设我们要设置一些环境变量和系统属性。在我们的测试代码开始之前,我们最终需要两个层次的嵌套:
@Test
void multipleSystemLambdas() throws Exception {
restoreSystemProperties(() -> {
withEnvironmentVariable("URL", "https://www.baeldung.com")
.execute(() -> {
System.setProperty("log_dir", "test/resources");
assertEquals("test/resources", System.getProperty("log_dir"));
assertEquals("https://www.baeldung.com", System.getenv("URL"));
});
});
}
在这里,JUnit插件或扩展可以帮助我们减少测试中所需的代码量。
2.3。使用更少的样板
我们应该期望能够以最少的样板编写测试:
@SystemStub
private EnvironmentVariables environmentVariables = ...;
@SystemStub
private SystemProperties restoreSystemProperties;
@Test
void multipleSystemStubs() {
System.setProperty("log_dir", "test/resources");
assertEquals("test/resources", System.getProperty("log_dir"));
assertEquals("https://www.baeldung.com", System.getenv("ADDRESS"));
}
这种方法是由SystemStubs
JUnit 5扩展提供的,它使我们的测试可以用更少的代码来组成。
2.4。测试生命周期挂钩
当唯一可用的工具是执行模式时,就不可能将存根行为挂钩到测试生命周期的所有部分。尝试将其与其他JUnit扩展(例如@SpringBootTest
结合使用时,这尤其具有挑战性。
如果我们想围绕Spring Boot测试设置一些环境变量,那么我们就不可能将整个测试生态系统合理地嵌入到一个测试方法中。我们需要一种方法来激活围绕测试套件的测试设置。
System Lambda所采用的方法永远不可能做到这一点,这是创建系统存根的主要原因之一。
2.5。鼓励动态特性
其他用于设置系统属性的框架,例如JUnit Pioneer ,则强调了编译时已知的配置。在可能使用Testcontainers或Wiremock的现代测试中,我们需要在这些工具启动后基于随机运行时设置来设置我们的系统属性。这与可在整个测试生命周期中使用的测试库一起使用时效果最佳。
2.6。更多可配置性
使用现成的测试配方(例如catchSystemExit
是有益的,这些配方可以环绕测试代码来完成一项工作。但是,这依赖于测试库开发人员来提供我们可能需要的每种配置选项。
按组成进行配置更灵活,并且是新的系统存根实现的很大一部分。
但是, System Stubs支持System Lambda的原始测试构造,以实现向后兼容。此外,它提供了新的JUnit 5扩展,一组JUnit 4规则以及许多其他配置选项。尽管基于原始代码,但已对其进行了大量重构和模块化,以提供更丰富的功能。
让我们更多地了解它。
3.入门
3.1 依赖关系
JUnit的5扩展需要一个合理高达最新版本的JUnit 5 :
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.6.2</version>
<scope>test</scope>
</dependency>
让我们将所有系统存根库依赖项添加到我们的pom.xml
:
<!-- for testing with only lambda pattern -->
<dependency>
<groupId>uk.org.webcompere</groupId>
<artifactId>system-stubs-core</artifactId>
<version>1.1.0</version>
<scope>test</scope>
</dependency>
<!-- for JUnit 4 -->
<dependency>
<groupId>uk.org.webcompere</groupId>
<artifactId>system-stubs-junit4</artifactId>
<version>1.1.0</version>
<scope>test</scope>
</dependency>
<!-- for JUnit 5 -->
<dependency>
<groupId>uk.org.webcompere</groupId>
<artifactId>system-stubs-jupiter</artifactId>
<version>1.1.0</version>
<scope>test</scope>
</dependency>
我们应该注意,我们只需要导入我们正在使用的测试框架所需的数量就可以了。的确,后两者在传递性上都包含核心依赖性。
现在让我们编写第一个测试。
3.2。 JUnit 4环境变量
我们可以通过在类型为EnvironmentVariablesRule
测试类中声明一个带有JUnit 4 @Rule
注释的字段来控制环境变量。这将在我们的测试运行时由JUnit 4激活,并允许我们在测试中设置环境变量:
@Rule
public EnvironmentVariablesRule environmentVariablesRule = new EnvironmentVariablesRule();
@Test
public void givenEnvironmentCanBeModified_whenSetEnvironment_thenItIsSet() {
environmentVariablesRule.set("ENV", "value1");
assertThat(System.getenv("ENV")).isEqualTo("value1");
}
实际上,我们可能更愿意在@Before
方法中设置环境变量值,以便可以在所有测试之间共享该设置:
@Before
public void before() {
environmentVariablesRule.set("ENV", "value1")
.set("ENV2", "value2");
}
在这里,我们应该注意使用流畅set
方法method chaining
轻松设置多个值成为可能。
我们还可以使用EnvironmentVariablesRule
对象的构造函数来提供有关构造的值:
@Rule
public EnvironmentVariablesRule environmentVariablesRule =
new EnvironmentVariablesRule("ENV", "value1",
"ENV2", "value2");
构造函数有多个重载,允许以不同的形式提供变量。 varargs
提供任意数量的名称-值对。
每个系统存根JUnit 4规则都是核心存根对象之一的子类。它们还可以在整个测试类的生命周期中使用, @ClassRule
在static
字段上使用@ClassRule批注,这将导致它们在第一个测试之前被激活,然后在最后一个测试之后被清除。
3.3。 JUnit 5环境变量
在JUnit 5测试中使用System Stubs对象之前,必须将扩展添加到我们的测试类中:
@ExtendWith(SystemStubsExtension.class)
class EnvironmentVariablesJUnit5 {
// tests
}
然后,我们可以在测试类中为JUnit 5创建一个字段以供我们管理。 @SystemStub
对此进行注释,以便扩展程序知道如何激活它:
@SystemStub
private EnvironmentVariables environmentVariables;
该扩展将仅管理标有@SystemStub
对象,这允许我们根据需要在测试中手动使用其他System Stubs对象。
在这里,我们没有提供存根对象的任何构造。该扩展为我们构造了一个,就像Mockito扩展构造了模拟一样。
现在,我们可以使用该对象来帮助我们在其中一项测试中设置环境变量:
@Test
void givenEnvironmentCanBeModified_whenSetEnvironment_thenItIsSet() {
environmentVariables.set("ENV", "value1");
assertThat(System.getenv("ENV")).isEqualTo("value1");
}
如果我们想提供从测试方法外部应用于所有测试的环境变量,则可以在@BeforeEach
方法内部进行操作,或者可以使用EnvironmentVariables
的构造函数来设置我们的值:
@SystemStub
private EnvironmentVariables environmentVariables =
new EnvironmentVariables("ENV", "value1");
与EnvironmentVariablesRule
,构造函数有多个重载,这使我们可以通过多种方法来设置所需的变量。如果愿意,我们还可以set
方法来设置值:
@SystemStub
private EnvironmentVariables environmentVariables =
new EnvironmentVariables()
.set("ENV", "value1")
.set("ENV2", "value2");
我们还可以将字段static
,以便将它们作为@BeforeAll
/ @AfterAll
生命周期的一部分进行管理。
3.4。 JUnit 5参数注入
当将存根对像用于所有测试时,将其放置在字段中很有用,但我们可能更喜欢仅将它们用于选定的对象。这可以通过JUnit 5参数注入来实现:
@Test
void givenEnvironmentCanBeModified(EnvironmentVariables environmentVariables) {
environmentVariables.set("ENV", "value1");
assertThat(System.getenv("ENV")).isEqualTo("value1");
}
在这种情况下, EnvironmentVariables
对象,从而使我们可以在单个测试中使用它。该对像也已激活,因此可以在运行时环境中运行。测试完成后,它将进行整理。
所有系统存根对像都具有默认构造函数,并且可以在运行时重新配置。我们可以根据需要注入任意数量的测试。
3.5。全面执行环境变量
SystemStubs
类获得用于创建存根的原始System Lambda外观方法。在内部,它们是通过创建存根对象的实例来实现的。有时,从配方返回的对象是存根对象,用于进一步的配置和使用:
withEnvironmentVariable("ENV3", "val")
.execute(() -> {
assertThat(System.getenv("ENV3")).isEqualTo("val");
});
在后台, withEnvironmentVariable
等效于:
return new EnvironmentVariables().set("ENV3", "val");
execute
方法是所有SystemStub
对象共有的。它设置对象定义的存根,然后执行传入的lambda。随后,它整理并把控制权返回给周围的测试。
如果测试代码返回一个值,那么可以通过execute
返回该值:
String extracted = new EnvironmentVariables("PROXY", "none")
.execute(() -> System.getenv("PROXY"));
assertThat(extracted).isEqualTo("none");
当我们正在测试的代码需要访问环境设置来构造某些东西时,这很有用。通常在测试诸如AWS Lambda处理程序之类的东西时使用,通常通过环境变量进行配置。
这种模式偶尔进行测试的优点是,我们必须仅在需要的地方显式设置存根。因此,它可以更加精确和可见。但是,它不允许我们在测试之间共享设置,并且可能会花费很多时间。
3.6。多个系统存根
我们已经了解了JUnit 4和JUnit 5插件如何为我们构造和激活存根对象。如果有多个存根,则框架代码将它们适当地设置和拆除。
但是,当我们为执行模式构造存根对象时,我们需要测试代码才能在其中全部运行。
这可以使用with
/ execute
方法来实现。 execute
使用多个存根对象创建一个复合对象而实现的:
with(new EnvironmentVariables("FOO", "bar"), new SystemProperties("prop", "val"))
.execute(() -> {
assertThat(System.getenv("FOO")).isEqualTo("bar");
assertThat(System.getProperty("prop")).isEqualTo("val");
});
现在,我们已经看到了使用System Stubs对象的一般形式,无论是否支持JUnit框架,让我们看看该库的其余功能。
4.系统属性
我们可以随时用Java System.setProperty
但是,这冒着将设置从一项测试泄漏到另一项测试的风险。 SystemProperties
存根控件的主要目的是在测试完成后将系统属性还原到其原始设置。但是,对于通用设置代码来说,定义在测试开始之前应使用哪些系统属性也很有用。
4.1。 JUnit 4系统属性
通过将规则添加到JUnit 4测试类,我们可以将每个测试与其他测试方法中进行的System.setProperty
我们还可以通过构造函数提供一些前期属性:
@Rule
public SystemPropertiesRule systemProperties =
new SystemPropertiesRule("db.connection", "false");
使用此对象,我们还可以在JUnit @Before
方法中设置一些其他属性:
@Before
public void before() {
systemProperties.set("before.prop", "before");
}
我们还可以在set
方法,或者根据需要使用System.setProperty
。我们只能在创建SystemPropertiesRule
或@Before
set
,因为它会将设置存储在规则中,以备后用。
4.2。 JUnit 5系统属性
我们有两个使用SystemProperties
对象的主要用例。我们可能希望在每个测试用例之后重置系统属性,或者我们希望在中央放置一些通用的系统属性以供每个测试用例使用。
恢复系统属性需要我们将JUnit 5扩展和SystemProperties
字段都添加到我们的测试类中:
@ExtendWith(SystemStubsExtension.class)
class RestoreSystemProperties {
@SystemStub
private SystemProperties systemProperties;
}
现在,每个测试将具有其以后更改的所有系统属性。
我们还可以通过参数注入对选定的测试执行此操作:
@Test
void willRestorePropertiesAfter(SystemProperties systemProperties) {
}
如果要在测试中设置属性,则可以在构造SystemProperties
对象@BeforeEach
方法:
@ExtendWith(SystemStubsExtension.class)
class SetSomeSystemProperties {
@SystemStub
private SystemProperties systemProperties;
@BeforeEach
void before() {
systemProperties.set("beforeProperty", "before");
}
}
再次提醒我们,JUnit 5测试需要使用@ExtendWith(SystemStubsExtension.class).
如果我们在初始化列表中new
语句,则扩展程序将创建System Stubs对象。
4.3。环顾四周的系统属性
SystemStubs
类提供了restoreSystemProperties
方法,以允许我们运行具有已还原属性的测试代码:
restoreSystemProperties(() -> {
// test code
System.setProperty("unrestored", "true");
});
assertThat(System.getProperty("unrestored")).isNull();
这需要一个不返回任何值的lambda。如果我们希望使用一个通用的设置函数来创建属性,从测试方法中获取返回值,或者with
/ execute
SystemProperties
与其他存根相结合,则可以显式创建该对象:
String result = new SystemProperties()
.execute(() -> {
System.setProperty("unrestored", "true");
return "it works";
});
assertThat(result).isEqualTo("it works");
assertThat(System.getProperty("unrestored")).isNull();
4.4。文件中的属性
SystemProperties
和EnvironmentVariables
对像都可以从Map
构造。这允许将Java的Properties
对象提供为系统属性或环境变量的源。
PropertySource
类内部有帮助程序方法,用于从文件或资源中加载Java属性。这些属性文件是名称/值对:
name=baeldung
version=1.0
我们可以使用fromResource
函数test.properties
SystemProperties systemProperties =
new SystemProperties(PropertySource.fromResource("test.properties"));
PropertySource
,其他来源也有类似的便捷方法fromFile
或fromInputStream
。
5.系统输出和系统错误
当我们的应用程序写入System.out,
可能很难进行测试。有时可以通过使用接口作为输出目标并在测试时进行模拟来解决此问题:
interface LogOutput {
void write(String line);
}
class Component {
private LogOutput log;
public void method() {
log.write("Some output");
}
}
Mockito
这样的技术可以很好地工作,但是如果我们只能捕获System.out
本身,则没有必要。
5.1。 JUnit 4 SystemOutRule
和SystemErrRule
为了在JUnit 4测试中将输出捕获到System.out
SystemOutRule
:
@Rule
public SystemOutRule systemOutRule = new SystemOutRule();
之后,可以在测试中读取System.out
System.out.println("line1");
System.out.println("line2");
assertThat(systemOutRule.getLines())
.containsExactly("line1", "line2");
我们可以选择文本格式。上面的示例使用getLines
提供Stream<String>
。我们也可以选择获取整个文本块:
assertThat(systemOutRule.getText())
.startsWith("line1");
但是,我们应该注意,此文本将包含换行符,这些字符在不同平台之间有所不同。我们可以在每个平台上使用归一化形式将换行符替换为\n
assertThat(systemOutRule.getLinesNormalized())
.isEqualTo("line1\nline2\n");
SystemErrRule
System.err
中的工作方式与在System.out
对应方式相同:
@Rule
public SystemErrRule systemErrRule = new SystemErrRule();
@Test
public void whenCodeWritesToSystemErr_itCanBeRead() {
System.err.println("line1");
System.err.println("line2");
assertThat(systemErrRule.getLines())
.containsExactly("line1", "line2");
}
还有一个SystemErrAndOutRule
类,它将System.out
和System.err
同时挖掘到单个缓冲区中。
5.2。 JUnit 5示例
与其他System Stubs对像一样,我们只需要声明一个类型为SystemOut
或SystemErr
的字段或参数。这将为我们提供输出的捕获:
@SystemStub
private SystemOut systemOut;
@SystemStub
private SystemErr systemErr;
@Test
void whenWriteToOutput_thenItCanBeAsserted() {
System.out.println("to out");
System.err.println("to err");
assertThat(systemOut.getLines()).containsExactly("to out");
assertThat(systemErr.getLines()).containsExactly("to err");
}
我们还可以使用SystemErrAndOut
类将两组输出定向到同一缓冲区。
5.3。全面执行示例
SystemStubs
外观提供了一些功能,用于点按输出并将其作为String
返回:
@Test
void givenTapOutput_thenGetOutput() throws Exception {
String output = tapSystemOutNormalized(() -> {
System.out.println("a");
System.out.println("b");
});
assertThat(output).isEqualTo("a\nb\n");
}
我们应该注意,这些方法没有提供像原始对象本身一样丰富的接口。输出的捕获不能轻松地与其他存根(例如,设置环境变量)组合使用。
但是,可以直接使用SystemOut
, SystemErr,
和SystemErrAndOut
例如,我们可以将它们与一些SystemProperties
结合使用:
SystemOut systemOut = new SystemOut();
SystemProperties systemPrope
0 评论