Guide to the System Stubs Library – 系统存根库指南

最后修改: 2020年 11月 27日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

It can be hard to test our software when it depends on system resources like environment variables, system properties, or uses process-level operations like System.exit.

当我们的软件依赖于环境变量、系统属性等系统资源,或使用System.exit等进程级操作时,可能很难进行测试。

Java doesn’t provide a direct method for setting environment variables, and we run the risk of the values set in one test affecting the execution of another. Similarly, we may find ourselves avoiding writing JUnit tests for code that might perform a System.exit as there’s a chance it would abort the tests.

Java没有提供直接的方法来设置环境变量,我们有可能在一个测试中设置的值影响另一个测试的执行。同样,我们可能会发现自己避免为可能执行System.exit的代码编写JUnit测试,因为它有可能会中止测试。

The System Rules and System Lambda Libraries were early solutions to these problems. In this tutorial, we’ll look at a new fork of System Lambda called System Stubs, which provides a JUnit 5 alternative.

System Rules 和 System Lambda 库是解决这些问题的早期方案。在本教程中,我们将了解System Lambda的一个新分叉,即System Stubs,它提供了一个JUnit 5替代方案。

2. Why System Stubs?

2.为什么是系统存根?

2.1. System Lambda Is Not a JUnit Plugin

2.1.System Lambda不是一个JUnit插件

The original System Rules library was only usable with JUnit 4. It could still be used with JUnit Vintage under JUnit 5, but that required the continued creation of JUnit 4 tests. The creators of the library produced a test framework agnostic version called System Lambda, which was intended for use inside each test method:

最初的系统规则库只能在 JUnit 4 中使用。它仍然可以与JUnit Vintage在JUnit 5下使用,但这需要继续创建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
}

The test code is expressed as a lambda, passed to a method that sets up the necessary stubbing. The cleanup happens just before control is returned to the rest of the test method.

测试代码被表达为一个lambda,传递给一个设置了必要存根的方法。清理工作发生在控制权返回到测试方法的其余部分之前。

Though this works well in some cases, the approach has a few disadvantages.

虽然这在某些情况下效果不错,但这种方法有一些缺点。

2.2. Avoiding Extra Code

2.2.避免多余的代码

The benefit of the System Lambda approach is that there are some common recipes inside its factory class for performing specific types of tests. However, this leads to some code bloat when we want to use it across many test cases.

System Lambda方法的好处是,在其工厂类中有一些通用的配方,用于执行特定类型的测试。然而,当我们想在许多测试案例中使用它时,这导致了一些代码的膨胀。

Firstly, even if the test code itself doesn’t throw a checked exception, the wrapper method does, so all methods gain a throws Exception. Secondly, setting up the same rule across multiple tests requires code duplication. Each test needs to perform the same configuration independently.

首先,即使测试代码本身没有抛出检查过的异常,包装方法也会抛出,所以所有的方法都会获得一个throws Exception。其次,在多个测试中设置相同的规则需要代码的重复。每个测试都需要独立执行相同的配置。

However, the most cumbersome aspect of this approach comes when we try to set up more than one tool at a time. Let’s say we want to set some environment variables and system properties. We end up needing two levels of nesting before our test code starts:

然而,当我们试图一次设置一个以上的工具时,这种方法最麻烦的地方就来了。比方说,我们想设置一些环境变量和系统属性。在我们的测试代码开始之前,我们最终需要两层嵌套。

@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"));
            });
    });
}

This is where a JUnit plugin or extension can help us cut down the amount of code we need in our tests.

这就是JUnit插件或扩展可以帮助我们减少测试中的代码量的地方。

2.3. Using Less Boilerplate

2.3.少用模板

We should expect to be able to write our tests with a minimum of boilerplate:

我们应该期望能够用最少的模板来编写我们的测试。

@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"));
}

This approach is provided by the SystemStubs JUnit 5 extension and allows our tests to be composed with less code.

这种方法由SystemStubs JUnit 5扩展提供,并允许我们用更少的代码组成测试。

2.4. Test Lifecycle Hooks

2.4.测试生命周期钩子

When the only available tool is the execute-around pattern, it’s impossible to hook in the stubbing behavior to all parts of the test lifecycle. This is particularly challenging when trying to combine it with other JUnit extensions, like @SpringBootTest.

当唯一可用的工具是绕过执行模式时,不可能将存根行为与测试生命周期的所有部分挂钩。当试图将其与其他JUnit扩展(如@SpringBootTest)相结合时,这尤其具有挑战性。

If we wanted to set up some environment variables around a Spring Boot test, then there is no way we could reasonably embed that whole test eco-system inside a single test method. We would need a way to activate the test set-up around a test suite.

如果我们想围绕Spring Boot测试设置一些环境变量,那么我们就没有办法合理地将整个测试生态嵌入到一个测试方法中。我们需要一种方法来激活围绕测试套件的测试设置。

This was never going to be possible with the methodology employed by System Lambda and was one of the main reasons to create System Stubs.

这在System Lambda采用的方法中是不可能实现的,也是创建System Stubs的主要原因之一。

2.5. Encourage Dynamic Properties

2.5.鼓励动态属性

Other frameworks for setting system properties, such as JUnit Pioneer, emphasize configurations known at compile time. In modern tests, where we may be using Testcontainers or Wiremock, we need to set up our system properties based on random runtime settings after those tools startup. This works best with a test library that can be used across the whole test lifecycle.

其他用于设置系统属性的框架,例如JUnit Pioneer,强调了在编译时已知的配置。在现代测试中,我们可能会使用TestcontainersWiremock,我们需要在这些工具启动后根据随机运行时设置来设置我们的系统属性。这与一个可以在整个测试生命周期中使用的测试库的效果最好。

2.6. More Configurability

2.6.更多的可配置性

It’s beneficial having ready-made test recipes, like catchSystemExit, which wrap around test code to do a single job. However, this relies on the test library developers to provide each variation of configuration option we might need.

拥有现成的测试配方是很有好处的,比如catchSystemExit,它可以环绕测试代码来完成一项工作。然而,这依赖于测试库开发人员提供我们可能需要的每一个配置选项的变化。

Configuration by composition is more flexible and is a large part of the new System Stubs implementation.

通过组合进行配置更加灵活,是新的系统存根实现的一个重要部分。

However, System Stubs supports the original test constructs from System Lambda for backward compatibility. Additionally, it provides a new JUnit 5 extension, a set of JUnit 4 rules, and many more configuration options. Though based on the original code, it has been heavily refactored and modularised to provide a richer set of features.

然而,System Stubs支持来自System Lambda的原始测试结构,以便向后兼容。此外,它还提供了一个新的JUnit 5扩展,一套JUnit 4规则,以及更多的配置选项。虽然基于原始代码,但它已经进行了大量的重构和模块化,以提供更丰富的功能集。

Let’s learn more about it.

让我们进一步了解一下。

3. Getting Started

3.开始工作

3.1. Dependencies

3.1. 依赖性

The JUnit 5 extension requires a reasonably up to date version of JUnit 5:

JUnit 5 扩展需要一个合理的最新版本的JUnit 5

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.8.1</version>
    <scope>test</scope>
</dependency>

Let’s add all the System Stubs library dependencies to our pom.xml:

让我们把所有的系统存根库依赖性添加到我们的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>

We should note that we only need to import as many of these as we need for the test framework we’re using. Indeed, both the latter two transitively include the core dependency.

我们应该注意到,我们只需要导入我们所使用的测试框架所需要的这些东西就可以了。事实上,后两者都包含了核心的依赖性。

Now let’s write our first test.

现在我们来写第一个测试。

3.2. JUnit 4 Environment Variables

3.2.JUnit 4环境变量

We can control environment variables by declaring a JUnit 4 @Rule annotated field in our test class of type EnvironmentVariablesRule. This will be activated by JUnit 4 when our tests run and will allow us to set environment variables inside the test:

我们可以通过在我们的测试类中声明一个JUnit 4 @Rule 注解的EnvironmentVariablesRule类型的字段来控制环境变量。这将在我们的测试运行时被JUnit 4激活,并允许我们在测试中设置环境变量。

@Rule
public EnvironmentVariablesRule environmentVariablesRule = new EnvironmentVariablesRule();

@Test
public void givenEnvironmentCanBeModified_whenSetEnvironment_thenItIsSet() {
    environmentVariablesRule.set("ENV", "value1");

    assertThat(System.getenv("ENV")).isEqualTo("value1");
}

In practice, we may prefer to set the environment variable values in a @Before method so that the set-up can be shared across all tests:

在实践中,我们可能更喜欢在@Before方法中设置环境变量值,这样设置可以在所有测试中共享。

@Before
public void before() {
    environmentVariablesRule.set("ENV", "value1")
      .set("ENV2", "value2");
}

Here we should note using the fluent set method, which makes setting multiple values easy through method chaining.

这里我们应该注意使用流畅的set方法,它通过方法链使设置多个值变得简单。

We can also use the constructor of the EnvironmentVariablesRule object to provide values on construction:

我们也可以使用EnvironmentVariablesRule对象的构造器来提供构造时的值。

@Rule
public EnvironmentVariablesRule environmentVariablesRule =
  new EnvironmentVariablesRule("ENV", "value1",
    "ENV2", "value2");

There are several overloads of the constructor, allowing variables to be provided in different forms. The one in the above example allows any number of name-value pairs to be provided using varargs.

该构造函数有几个重载,允许以不同形式提供变量。上面例子中的一个允许使用varargs提供任意数量的名-值对。

Each of the System Stubs JUnit 4 rules is a subclass of one of the core stubbing objects. They can also be used across the lifecycle of a whole test class with the @ClassRule annotation on a static field, which will cause them to be activated before the first test, and then cleaned up just after the last.

每个系统存根JUnit 4规则都是一个核心存根对象的子类。它们也可以通过@ClassRule注解static字段上跨整个测试类的生命周期使用,这将导致它们在第一次测试前被激活,然后在最后一次测试后被清理掉。

3.3. JUnit 5 Environment Variables

3.3.JUnit 5环境变量

Before we use System Stubs objects inside a JUnit 5 test, we must add the extension to our test class:

在我们在JUnit 5测试中使用System Stubs对象之前,我们必须将该扩展添加到我们的测试类。

@ExtendWith(SystemStubsExtension.class)
class EnvironmentVariablesJUnit5 {
    // tests
}

Then we can create a field in the test class for JUnit 5 to manage for us. We annotate this with @SystemStub so that the extension knows to activate it:

然后我们可以在测试类中创建一个字段,让JUnit 5为我们管理。我们用@SystemStub来注解这个字段,这样扩展就知道要激活它。

@SystemStub
private EnvironmentVariables environmentVariables;

The extension will only manage objects marked with @SystemStub, which allows us to use other System Stubs objects in the test manually if we prefer.

该扩展将只管理标有@SystemStub的对象,如果我们愿意,可以在测试中手动使用其他System Stubs对象。

Here, we haven’t provided any construction of the stub object. The extension constructs one for us, in the same way as the Mockito extension constructs mocks.

在这里,我们没有提供存根对象的任何构造。该扩展为我们构造了一个,与Mockito扩展构造mocks的方式相同。

We can now use the object to help us set environment variables inside one of our tests:

现在我们可以使用该对象来帮助我们在一个测试中设置环境变量。

@Test
void givenEnvironmentCanBeModified_whenSetEnvironment_thenItIsSet() {
    environmentVariables.set("ENV", "value1");

    assertThat(System.getenv("ENV")).isEqualTo("value1");
}

If we wanted to provide the environment variables that apply to all tests from outside of the test method, we can do that inside a @BeforeEach method or can use the constructor of EnvironmentVariables to set our values:

如果我们想在测试方法之外提供适用于所有测试的环境变量,我们可以在@BeforeEach方法中完成,或者可以使用EnvironmentVariables的构造函数来设置我们的值。

@SystemStub
private EnvironmentVariables environmentVariables =
  new EnvironmentVariables("ENV", "value1");

As with EnvironmentVariablesRule there are several overloads of the constructor, allowing us many ways to set the desired variables. We can also use the set method fluently to set values if we prefer:

EnvironmentVariablesRule一样,构造函数有几个重载,允许我们以多种方式设置所需的变量。如果我们愿意,我们也可以流畅地使用set方法来设置数值。

@SystemStub
private EnvironmentVariables environmentVariables =
  new EnvironmentVariables()
    .set("ENV", "value1")
    .set("ENV2", "value2");

We can also make our fields static for them to be managed as part of the @BeforeAll/@AfterAll lifecycle.

我们还可以使我们的字段静态,以便将它们作为@BeforeAll/@AfterAll生命周期的一部分来管理。

3.4. JUnit 5 Parameter Injection

3.4 JUnit 5 参数注入

While placing the stub objects in fields is useful when using them for all of our tests, we may prefer to use them only for selected ones. This can be achieved by JUnit 5 parameter injection:

虽然将存根对象放置在字段中,对我们所有的测试都很有用,但我们可能更喜欢只在选定的测试中使用它们。这可以通过JUnit 5参数注入来实现。

@Test
void givenEnvironmentCanBeModified(EnvironmentVariables environmentVariables) {
    environmentVariables.set("ENV", "value1");

    assertThat(System.getenv("ENV")).isEqualTo("value1");
}

In this case, the EnvironmentVariables object was constructed for us with its default constructor, allowing us to use it within a single test. The object has also been activated so that it is operating on the runtime environment. It will be tidied up when the test is finished.

在这种情况下,EnvironmentVariables对象是用其默认构造函数为我们构造的,允许我们在单个测试中使用它。该对象也已被激活,以便它在运行时环境中运行。测试结束后,它将被整理好。

All of the System Stubs objects have a default constructor and the ability to be reconfigured while running. We can inject as many as we need into our tests.

所有的系统存根对象都有一个默认的构造函数,并且能够在运行时进行重新配置。我们可以根据需要向我们的测试注入尽可能多的对象。

3.5. Execute-Around Environment Variables

3.5.围绕环境变量的执行

The original System Lambda facade methods for creating stubs are also available via the SystemStubs class. Internally they are implemented by creating instances of the stubbing objects. Sometimes the object returned from the recipe is a stub object for further configuration and use:

创建存根的原始System Lambda Facade方法也可以通过SystemStubs类获得。在内部,它们是通过创建存根对象的实例来实现的。有时,从配方中返回的对象是一个存根对象,以便进一步配置和使用。

withEnvironmentVariable("ENV3", "val")
    .execute(() -> {
        assertThat(System.getenv("ENV3")).isEqualTo("val");
    });

Behind the scenes, withEnvironmentVariable is doing the equivalent of:

在幕后,withEnvironmentVariable正在做相当于。

return new EnvironmentVariables().set("ENV3", "val");

The execute method is common to all SystemStub objects. It sets up the stubbing defined by the object, then executes the lambda passed in. Afterward, it tidies up and returns control to the surrounding test.

execute方法是所有SystemStub对象所共有的。它设置了该对象所定义的存根,然后执行传入的lambda。之后,它将整理并将控制权返回给周围的测试。

If the test code returns a value, then that value can be returned by execute:

如果测试代码返回一个值,那么该值可以由execute返回。

String extracted = new EnvironmentVariables("PROXY", "none")
  .execute(() -> System.getenv("PROXY"));

assertThat(extracted).isEqualTo("none");

This can be useful when the code we are testing needs to have access to environment settings to construct something. It’s commonly used when testing things like AWS Lambda handlers, which are often configured through environment variables.

当我们测试的代码需要访问环境设置来构建一些东西时,这可能很有用。在测试AWS Lambda处理程序等事物时,通常使用这种方法,这些处理程序通常是通过环境变量配置的。

The advantage of this pattern for occasional tests is that we have to set up the stubbing explicitly, only where needed. Therefore it can be more precise and visible. However, it does not allow us to share the setup between tests and can be more long-winded.

这种模式对于偶尔的测试的好处是,我们必须明确设置存根,只在需要的地方设置。因此,它可以更加精确和明显。然而,它不允许我们在测试之间共享设置,而且可能会比较啰嗦。

3.6. Multiple System Stubs

3.6 多个系统存根

We have already seen how the JUnit 4 and JUnit 5 plugins construct and activate stubbing objects for us. If there are multiple stubs, they are set up and torn down appropriately by the framework code.

我们已经看到了JUnit 4和JUnit 5插件如何为我们构造和激活存根对象。如果有多个存根,它们会被框架代码适当地建立和拆毁。

However, when we construct stubbing objects for the execute-around pattern, we need our test code to run inside them all.

然而,当我们为执行-环绕模式构建存根对象时,我们需要我们的测试代码在它们里面全部运行。

This can be achieved using the with/execute methods. These work by creating a composite from multiple stubbing objects used with a single execute:

这可以使用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");
  });

Now we have seen the general form of using the System Stubs objects, both with and without JUnit framework support, let’s look at the rest of the library’s capabilities.

现在我们已经看到了使用System Stubs对象的一般形式,包括有无JUnit框架支持,让我们看看该库的其他功能。

4. System Properties

4.系统属性

We can call System.setProperty at any time in Java. However, this runs the risk of leaking the settings out of one test into another. The primary aim of SystemProperties stubbing is to restore the system properties to their original settings after the test is complete. However, it’s also useful for common setup code to define which system properties should be used before the test starts.

我们可以在Java中随时调用System.setProperty。然而,这有可能将设置从一个测试中泄露到另一个测试中。SystemProperties存根的主要目的是在测试完成后将系统属性恢复到其原始设置。然而,在测试开始前,定义哪些系统属性应该被使用,这对普通的设置代码也很有用。

4.1. JUnit 4 System Properties

4.1 JUnit 4系统属性

By adding the rule to the JUnit 4 test class, we can insulate each test from any System.setProperty calls made in other test methods. We can also provide some upfront properties via the constructor:

通过将该规则添加到JUnit 4测试类中,我们可以将每个测试与其他测试方法中的任何System.setProperty调用隔离。我们还可以通过构造函数提供一些前期的属性。

@Rule
public SystemPropertiesRule systemProperties =
  new SystemPropertiesRule("db.connection", "false");

With this object, we can also set some additional properties in the JUnit @Before method:

有了这个对象,我们还可以在JUnit @Before方法中设置一些额外的属性。

@Before
public void before() {
    systemProperties.set("before.prop", "before");
}

We can also use the set method in the body of a test or use System.setProperty if we wish. We must only use set in creating the SystemPropertiesRule, or in the @Before method, as it stores the setting in the rule, ready for applying later.

我们也可以在测试的主体中使用set方法,或者根据需要使用System.setProperty。我们只能在创建SystemPropertiesRule时,或在@Before方法中使用set,因为它将设置保存在规则中,准备以后应用。

4.2. JUnit 5 System Properties

4.2 JUnit 5系统属性

We have two main use cases for using the SystemProperties object. We may wish to reset the system properties after each test case, or we may wish to prepare some common system properties in a central place for each test case to use.

我们在使用SystemProperties对象时有两个主要用例。我们可能希望在每个测试用例后重置系统属性,或者我们可能希望在一个中心位置准备一些通用的系统属性,供每个测试用例使用。

Restoring system properties requires us to add both the JUnit 5 extension and a SystemProperties field to our test class:

恢复系统属性需要我们同时添加JUnit 5扩展和SystemProperties字段到我们的测试类。

@ExtendWith(SystemStubsExtension.class)
class RestoreSystemProperties {
    @SystemStub
    private SystemProperties systemProperties;

}

Now, each test will have any system properties it changes cleaned up afterward.

现在,每个测试都会有它所改变的任何系统属性在之后被清理掉。

We can also do this for selected tests by parameter injection:

我们也可以通过参数注入对选定的测试进行这样的处理。

@Test
void willRestorePropertiesAfter(SystemProperties systemProperties) {

}

If we want the test to have properties set in it, then we can either assign those properties in the construction of our SystemProperties object or use a @BeforeEach method:

如果我们希望测试中设置有属性,那么我们可以在构建SystemProperties对象时分配这些属性,或者使用@BeforeEach方法。

@ExtendWith(SystemStubsExtension.class)
class SetSomeSystemProperties {
    @SystemStub
    private SystemProperties systemProperties;

    @BeforeEach
    void before() {
        systemProperties.set("beforeProperty", "before");
    }
}

Again, let’s note that the JUnit 5 test needs to be annotated with @ExtendWith(SystemStubsExtension.class). The extension will create the System Stubs object if we do not provide a new statement in the initializer list.

我们再次注意到,JUnit 5测试需要用@ExtendWith(SystemStubsExtension.class)来注释。如果我们不在初始化器列表中提供一个new语句,扩展将创建System Stubs对象。

4.3. System Properties with Execute Around

4.3.围绕执行的系统属性

The SystemStubs class provides a restoreSystemProperties method to allow us to run test code with properties restored:

SystemStubs类提供了一个restoreSystemProperties方法,使我们能够在恢复属性后运行测试代码。

restoreSystemProperties(() -> {
    // test code
    System.setProperty("unrestored", "true");
});

assertThat(System.getProperty("unrestored")).isNull();

This takes a lambda that returns nothing. If we wish to use a common set-up function to create properties, get a return value from the test method, or combine SystemProperties with other stubs via with/execute, then we can create the object explicitly:

这需要一个lambda,它不返回任何东西。如果我们希望使用普通的设置函数来创建属性,从测试方法中获取返回值,或者通过with/executeSystemProperties与其他存根相结合,那么我们可以显式创建对象。

String result = new SystemProperties()
  .execute(() -> {
      System.setProperty("unrestored", "true");
      return "it works";
  });

assertThat(result).isEqualTo("it works");
assertThat(System.getProperty("unrestored")).isNull();

4.4. Properties in Files

4.4.文件中的属性

Both the SystemProperties and EnvironmentVariables objects can be constructed from a Map. This allows Java’s Properties object to be provided as the source of either system properties or environment variables.

SystemPropertiesEnvironmentVariables对象都可以从Map构建。这使得Java的Properties对象可以作为系统属性或环境变量的来源。

There are helper methods inside the PropertySource class for loading up Java properties from files or resources. These properties files are name/value pairs:

PropertySource类中有一些辅助方法,用于从文件或资源中加载Java属性。这些属性文件是名称/值对。

name=baeldung
version=1.0

We can load from the resource test.properties by using the fromResource function:

我们可以通过使用fromResource函数从资源test.properties加载。

SystemProperties systemProperties =
  new SystemProperties(PropertySource.fromResource("test.properties"));

There are similar convenience methods in PropertySource for other sources, such as fromFile or fromInputStream.

PropertySource中也有类似的便利方法,用于其他来源,例如fromFilefromInputStream

5. System Out and System Err

系统输出和系统错误

When our application writes to System.out, it can be hard to test. This is sometimes solved by using an interface as the target of output and mocking that at test time:

当我们的应用程序写到System.out时,可能很难测试。这有时可以通过使用一个接口作为输出的目标,并在测试时对其进行模拟来解决。

interface LogOutput {
   void write(String line);
}

class Component {
    private LogOutput log;

    public void method() {
        log.write("Some output");
    }
}

Techniques like this work well with Mockito mocks but are not necessary if we can just trap System.out itself.

像这样的技术在Mockito模拟中运行良好,但如果我们可以直接捕获System.out本身,则没有必要。

5.1. JUnit 4 SystemOutRule and SystemErrRule

5.1.JUnit 4 SystemOutRuleSystemErrRule

To trap output to System.out in a JUnit 4 test, we add the SystemOutRule:

为了在JUnit 4测试中捕获输出到System.out,我们添加SystemOutRule

@Rule
public SystemOutRule systemOutRule = new SystemOutRule();

After that, any output to System.out can be read within the test:

之后,任何输出到System.out的内容都可以在测试中读取。

System.out.println("line1");
System.out.println("line2");

assertThat(systemOutRule.getLines())
  .containsExactly("line1", "line2");

We have a choice of formats for the text. The above example uses the Stream<String> provided by getLines. We may also choose to get the whole block of text:

我们可以选择文本的格式。上面的例子使用了Stream<String>,由getLines提供。我们也可以选择获取整个文本块。

assertThat(systemOutRule.getText())
  .startsWith("line1");

However, we should note that this text will have newline characters that vary between platforms. We can replace newlines with \n on every platform by using the normalized form:

然而,我们应该注意,这个文本会有换行符,而换行符在不同的平台上有所不同。我们可以通过使用规范化的形式在每个平台上用n替换换行符。

assertThat(systemOutRule.getLinesNormalized())
  .isEqualTo("line1\nline2\n");

The SystemErrRule works in the same way for System.err as its System.out counterpart:

SystemErrRuleSystem.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");
}

There is also a SystemErrAndOutRule class, which taps both System.out and System.err simultaneously into a single buffer.

还有一个SystemErrAndOutRule类,它将System.outSystem.err同时接入一个缓冲器。

5.2. JUnit 5 Example

5.2.JUnit 5示例

As with the other System Stubs objects, we only need to declare a field or parameter of type SystemOut or SystemErr. This will provide us with a capture of the output:

与其他系统存根对象一样,我们只需要声明一个SystemOutSystemErr类型的字段或参数。这将为我们提供对输出的捕获。

@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");
}

We can also use the SystemErrAndOut class to direct both sets of output into the same buffer.

我们也可以使用SystemErrAndOut类来引导两组输出进入同一个缓冲区。

5.3. Execute-Around Example

5.3.环绕执行示例

The SystemStubs facade provides some functions for tapping the output and returning it as a String:

SystemStubs Facade提供了一些函数,用于敲击输出并将其作为String返回。

@Test
void givenTapOutput_thenGetOutput() throws Exception {
    String output = tapSystemOutNormalized(() -> {
        System.out.println("a");
        System.out.println("b");
    });

    assertThat(output).isEqualTo("a\nb\n");
}

We should note that these methods do not provide as rich an interface as the raw objects themselves. The capture of output can’t easily be combined with other stubbing, such as setting environment variables.

我们应该注意到,这些方法并没有提供像原始对象本身那样丰富的接口。对输出的捕获不能轻易地与其他存根相结合,例如设置环境变量。

However, the SystemOut, SystemErr, and SystemErrAndOut objects can be used directly. For example, we could combine them with some SystemProperties:

然而,SystemOutSystemErrSystemErrAndOut对象可以直接使用。例如,我们可以将它们与一些SystemProperties相结合。

SystemOut systemOut = new SystemOut();
SystemProperties systemProperties = new SystemProperties("a", "!");
with(systemOut, systemProperties)
  .execute(()  -> {
    System.out.println("a: " + System.getProperty("a"));
});

assertThat(systemOut.getLines()).containsExactly("a: !");

5.4. Muting

5.4.缄默

Sometimes our aim is not to capture output but to keep it from cluttering our test run logs. We can achieve this using the muteSystemOut or muteSystemErr functions:

有时,我们的目的不是为了捕获输出,而是为了不让它扰乱我们的测试运行日志。我们可以使用muteSystemOutmuteSystemErr函数来实现这一目标。

muteSystemOut(() -> {
    System.out.println("nothing is output");
});

We can achieve the same thing across all tests via the JUnit 4 SystemOutRule:

我们可以通过JUnit 4 SystemOutRule在所有测试中实现同样的事情。

@Rule
public SystemOutRule systemOutRule = new SystemOutRule(new NoopStream());

In JUnit 5, we can use the same technique:

在JUnit 5中,我们可以使用同样的技术。

@SystemStub
private SystemOut systemOut = new SystemOut(new NoopStream());

5.5. Customization

5.5.定制化

As we have seen, there are several variations for intercepting output. They all share a common base class in the library. For convenience, several helper methods and types, like SystemErrAndOut, help do common things. However, the library itself is easily customized.

正如我们所看到的,拦截输出有几种变化。它们都在库中共享一个共同的基类。为了方便起见,一些辅助方法和类型,如SystemErrAndOut,帮助做一些常见的事情。然而,该库本身很容易定制。

We could provide our own target for capturing the output as an implementation of Output. We’ve already seen the Output class TapStream in use in the first examples. NoopStream is used for muting. We also have DisallowWriteStream that throws an error if something writes to it:

我们可以提供我们自己的目标来捕获输出,作为Output的实现。我们已经看到OutputTapStream在第一个例子中的使用。NoopStream用于静音。我们还有DisallowWriteStream,如果有东西写到它,它会抛出一个错误。

// throws an exception:
new SystemOut(new DisallowWriteStream())
  .execute(() -> System.out.println("boo"));

6. Mocking System In

6.嘲讽系统在

We may have an application that reads input on stdin. Testing this could involve extracting the algorithm into a function that reads from any InputStream and then feeding it with a pre-preprepared input stream. In general, modular code is better, so this is a good pattern.

我们可能有一个应用程序在stdin上读取输入。测试时可以将算法提取到一个从任何InputStream中读取的函数中,然后用事先准备好的输入流来喂它。一般来说,模块化的代码更好,所以这是一个好的模式。

However, if we only test the core functions, we lose test coverage on the code which provides System.in as the source.

然而,如果我们只测试核心函数,就会失去对提供System.in作为源的代码的测试覆盖。

In any case, it can be inconvenient to construct our own streams. Luckily, System Stubs has solutions for all of these.

在任何情况下,构建我们自己的流都是不方便的。幸运的是,System Stubs对所有这些都有解决方案。

6.1. Test Input Streams

6.1.测试输入流

System Stubs provides a family of AltInputStream classes as alternative inputs for any code that reads from an InputStream:

系统存根提供了一系列AltInputStream类,作为InputStream读取的任何代码的替代性输入。

LinesAltStream testInput = new LinesAltStream("line1", "line2");

Scanner scanner = new Scanner(testInput);
assertThat(scanner.nextLine()).isEqualTo("line1");

In this example, we’ve used an array of strings to construct LinesAltStream, but we could have supplied the input from a Stream<String>, allowing this to be used with any source of text data without necessarily loading it all into memory at once.

在这个例子中,我们使用了一个字符串数组来构造LinesAltStream,但是我们可以从一个Stream<String>中提供输入,这样就可以用于任何文本数据的来源,而不一定要一次性将其全部加载到内存。

6.2. JUnit 4 Example

6.2.JUnit 4实例

We can provide lines for input in a JUnit 4 test using the SystemInRule:

我们可以在JUnit 4测试中使用SystemInRule为输入提供行。

@Rule
public SystemInRule systemInRule =
  new SystemInRule("line1", "line2", "line3");

Then, the test code can read this input from System.in:

然后,测试代码可以从System.in读取这个输入。

@Test
public void givenInput_canReadFirstLine() {
    assertThat(new Scanner(System.in).nextLine())
      .isEqualTo("line1");
}

6.3. JUnit 5 Example

6.3.JUnit 5示例

For JUnit 5 tests, we create a SystemIn field:

对于JUnit 5测试,我们创建一个SystemIn字段。

@SystemStub
private SystemIn systemIn = new SystemIn("line1", "line2", "line3");

Then our tests will run with System.in providing these lines as input.

然后我们的测试将在System.in提供这些行作为输入时运行。

6.4. Execute-Around Example

6.4.环绕执行示例

The SystemStubs facade provides withTextFromSystemIn as a factory method that creates a SystemIn object for use with its execute method:

SystemStubs Facade 提供了 withTextFromSystemIn 作为工厂方法,用于创建 SystemIn 对象,供其 execute 方法使用。

withTextFromSystemIn("line1", "line2", "line3")
  .execute(() -> {
      assertThat(new Scanner(System.in).nextLine())
        .isEqualTo("line1");
  });

6.5. Customization

6.5.定制化

More features can be added to the SystemIn object either on construction or while it is running within a test.

更多的功能可以在构建时或在测试中运行时添加到SystemIn对象。

We can call andExceptionThrownOnInputEnd, which causes reading from System.in to throw an exception when it runs out of text. This can simulate an interrupted read from a file.

我们可以调用andExceptionThrownOnInputEnd,使从System.in读取的文本用完后抛出一个异常。这可以模拟从文件中中断的读取。

We can also set the input stream to come from any InputStream, like FileInputStream, by using setInputStream. We also have LinesAltStream and TextAltStream, which operate on the input text.

我们也可以通过使用setInputStream将输入流设置为来自任何InputStream,例如FileInputStream。我们还有LinesAltStreamTextAltStream,它们对输入文本进行操作。

7. Mocking System.Exit

7.嘲弄System.Exit

As mentioned previously, if our code can call System.exit, it can make for dangerous and hard to debug test faults. One of our aims in stubbing System.exit is to make an accidental call into a traceable error. Another motivation is to test intentional exits from the software.

如前所述,如果我们的代码可以调用System.exit,就会造成危险和难以调试的测试故障。我们存根System.exit的目的之一是使意外的调用成为可追踪的错误。另一个动机是测试有意退出软件的行为。

7.1. JUnit 4 Example

7.1.JUnit 4实例

Let’s add the SystemExitRule to a test class as a safety measure to prevent any System.exit from stopping the JVM:

让我们将SystemExitRule添加到测试类中,作为一项安全措施,防止任何System.exit停止JVM。

@Rule
public SystemExitRule systemExitRule = new SystemExitRule();

However, we may also wish to see if the right exit code was used. For that, we need to assert that the code throws the AbortExecutionException, which is the System Stubs signal that System.exit was called.

然而,我们可能还想看看是否使用了正确的退出代码。为此,我们需要断言该代码抛出了AbortExecutionException,这是System Stubs的信号,即System.exit被调用。

@Test
public void whenExit_thenExitCodeIsAvailable() {
    assertThatThrownBy(() -> {
        System.exit(123);
    }).isInstanceOf(AbortExecutionException.class);

    assertThat(systemExitRule.getExitCode()).isEqualTo(123);
}

In this example, we’ve used assertThatThrownBy from AssertJ to catch and check the exception signaling exit occurred. Then we looked at getExitCode from the SystemExitRule to assert the exit code.

在这个例子中,我们使用了来自AssertJassertThatThrownBy来捕捉和检查标志着退出发生的异常。然后我们从SystemExitRule中查看getExitCode来断言退出代码。

7.2. JUnit 5 Example

7.2.JUnit 5示例

For JUnit 5 tests, we declare the @SystemStub field:

对于JUnit 5测试,我们声明@SystemStub字段。

@SystemStub
private SystemExit systemExit;

Then we use the SystemExit class in the same way as SystemExitRule in JUnit 4. Given that the SystemExitRule class is a subclass of SystemExit, they have the same interface.

然后我们以JUnit 4中SystemExit类的方式使用SystemExitRule。鉴于SystemExitRule类是SystemExit的子类,它们具有相同的接口。

7.3. Execute-Around Example

7.3.环绕执行示例

The SystemStubs class provides catchSystemExit, which internally uses SystemExit‘s execute function:

SystemStubs类提供了catchSystemExit,它在内部使用SystemExitexecute函数。

int exitCode = catchSystemExit(() -> {
    System.exit(123);
});
assertThat(exitCode).isEqualTo(123);

Compared with the JUnit plugin examples, this code does not throw an exception to indicate a system exit. Instead, it catches the error and records the exit code. With the facade method, it returns the exit code.

与JUnit插件的例子相比,这段代码没有抛出一个异常来表示系统退出。相反,它捕捉错误并记录退出代码。通过facade方法,它返回退出代码。

When we use the execute method directly, the exit is caught, and the exit code is set inside the SystemExit object. We can then call getExitCode to get the exit code, or null if there was none.

当我们直接使用execute方法时,退出被捕获,退出代码被设置在SystemExit对象内。然后我们可以调用getExitCode来获取退出代码,如果没有退出代码,则调用null

8. Custom Test Resources in JUnit 5

8.JUnit 5中的自定义测试资源

JUnit 4 already provides a simple structure for creating test rules like the ones used in System Stubs. If we want to make a new test rule for some resource, with a setup and teardown, we can subclass ExternalResource and provide overrides of the before and after methods.

JUnit 4已经提供了一个创建测试规则的简单结构,就像在System Stubs中使用的那样。如果我们想为某些资源制定新的测试规则,并进行设置和拆除,我们可以将ExternalResource子类化,并提供beforeafter方法的覆盖。

JUnit 5 has a more complex pattern for resource management. For simple use cases, it’s possible to use the System Stubs library as a starting point. The SystemStubsExtension operates on anything that satisfies the TestResource interface.

JUnit 5对资源管理有一个更复杂的模式。对于简单的用例,可以使用System Stubs库作为起点。SystemStubsExtension对任何满足TestResource接口的东西进行操作。

8.1. Creating a TestResource

8.1.创建一个TestResource

We can create a subclass of TestResource and then use our custom objects in the same way we use System Stubs ones. We should note that we need to provide a default constructor if we want to use the automatic creation of fields and parameters.

我们可以创建一个TestResource的子类,然后以使用System Stubs的同样方式使用我们的自定义对象。我们应该注意,如果我们想使用自动创建字段和参数的方法,我们需要提供一个默认的构造函数。

Let’s say we wanted to open a connection to a database for some tests and close it afterward:

假设我们想为一些测试打开一个与数据库的连接,并在之后关闭它。

public class FakeDatabaseTestResource implements TestResource {
    // let's pretend this is a database connection
    private String databaseConnection = "closed";

    @Override
    public void setup() throws Exception {
        databaseConnection = "open";
    }

    @Override
    public void teardown() throws Exception {
        databaseConnection = "closed";
    }

    public String getDatabaseConnection() {
        return databaseConnection;
    }
}

We’re using the databaseConnection string as an illustration of a resource like a database connection. We modify the state of the resource in the setup and teardown methods.

我们使用databaseConnection字符串来说明像数据库连接这样的资源。我们在setupteardown方法中修改该资源的状态。

8.2. Execute-Around Is Built-In

8.2.内建了 “环绕执行 “功能

Now let’s try using this with the execute-around pattern:

现在让我们试试用这个模式来执行。

FakeDatabaseTestResource fake = new FakeDatabaseTestResource();
assertThat(fake.getDatabaseConnection()).isEqualTo("closed");

fake.execute(() -> {
    assertThat(fake.getDatabaseConnection()).isEqualTo("open");
});

As we can see, the TestResource interface gave it the execute-around capabilities of the other objects.

正如我们所看到的,TestResource接口给了它其他对象的执行-环绕能力。

8.3. Custom TestResource in JUnit 5 Test

8.3.JUnit 5测试中的自定义TestResource

We can also use this inside a JUnit 5 test:

我们也可以在JUnit 5测试中使用这个。

@ExtendWith(SystemStubsExtension.class)
class FakeDatabaseJUnit5UnitTest {

    @Test
    void useFakeDatabase(FakeDatabaseTestResource fakeDatabase) {
        assertThat(fakeDatabase.getDatabaseConnection()).isEqualTo("open");
    }
}

So, it is easy to create additional test objects that follow the System Stubs design.

因此,很容易创建额外的测试对象,遵循系统存根设计。

9. Environment and Property Overrides for JUnit 5 Spring Tests

9.JUnit 5 Spring测试的环境和属性重写

Setting environment variables for Spring tests can be difficult. We might compose a custom rule for integration testing to set some system properties for Spring to pick up.

为Spring测试设置环境变量可能很困难。我们可能会为集成测试编写一个自定义规则,以设置一些系统属性供Spring使用。

We may also use an ApplicationContextInitializer class to plug into our Spring Context, providing extra properties for the test.

我们还可以使用ApplicationContextInitializer类来插入我们的Spring Context,为测试提供额外属性。

As many Spring applications are controlled by system property or environment variable overrides, it may be easier to use System Stubs to set these in an outer test, with the Spring test running as an inner class.

由于许多Spring应用程序是由系统属性或环境变量重写控制的,因此使用System Stubs在外部测试中设置这些属性可能更容易,而Spring测试则作为内部类运行。

There’s a full example provided in the System Stubs documentation. We start by creating an outer class:

在System Stubs文档中提供了一个完整的例子。我们首先创建一个外层类。

@ExtendWith(SystemStubsExtension.class)
public class SpringAppWithDynamicPropertiesTest {

    // sets the environment before Spring even starts
    @SystemStub
    private static EnvironmentVariables environmentVariables;
}

In this instance, the @SystemStub field is static and is initialized in the @BeforeAll method:

在这个例子中,@SystemStub字段是静态的,在@BeforeAll方法中被初始化。

@BeforeAll
static void beforeAll() {
     String baseUrl = ...;

     environmentVariables.set("SERVER_URL", baseUrl);
}

This point in the test lifecycle allows some global resources to be created and applied to the running environment before the Spring test runs.

在测试生命周期的这一点上,允许在Spring测试运行前创建一些全局资源并应用于运行环境。

Then, we can put the Spring test into a @Nested class. This causes it to be run only when the parent class is set up:

然后,我们可以把Spring测试放到一个@Nested类中。这使得它只有在父类设置好后才会运行。

@Nested
@SpringBootTest(classes = {RestApi.class, App.class},
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class InnerSpringTest {
    @LocalServerPort
    private int serverPort;

    // Test methods
}

The Spring context is created against the state of the environment set by the @SystemStub objects in the outer class.

Spring上下文是针对外部类中的@SystemStub对象所设定的环境状态创建的。

This technique also allows us to control the configuration of any other libraries that depend on the state of system properties or environment variables that may be running behind Spring Beans.

这种技术还允许我们控制任何其他依赖系统属性状态或环境变量的库的配置,这些库可能在Spring Beans后面运行。

This can allow us to hook into the test lifecycle to modify things like proxy settings or HTTP connection pool parameters before a Spring test runs.

这可以让我们在Spring测试运行前钩住测试生命周期,以修改代理设置或HTTP连接池参数等事项

10. Conclusion

10.结语

In this article, we’ve looked at the importance of being able to mock system resources and how System Stubs allows for complex configurations of stubbing with a minimum of code repetition through its JUnit 4 and JUnit 5 plugins.

在这篇文章中,我们看了能够模拟系统资源的重要性,以及System Stubs如何通过其JUnit 4和JUnit 5插件,以最少的代码重复来实现复杂的存根配置。

We saw how to provide and isolate environment variables and system properties in our tests. Then we looked at capturing the output and controlling the input on the standard streams. We also looked at capturing and asserting calls to System.exit.

我们看到如何在我们的测试中提供和隔离环境变量和系统属性。然后我们看了捕捉输出和控制标准流的输入。我们还研究了捕获和断言对System.exit的调用。

Finally, we looked at how to create custom test resources and how to use System Stubs with Spring.

最后,我们研究了如何创建自定义测试资源以及如何使用Spring的系统存根。

As always, the full source code of the examples is available over on GitHub.

一如既往,这些示例的完整源代码可在GitHub上获得over