How to Mock Environment Variables in Unit Tests – 如何在单元测试中模拟环境变量

最后修改: 2023年 10月 23日

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

1. Overview

1.概述

When we’re unit testing code that depends on environment variables, we may wish to provide specific values for them as part of our test implementation.

在对依赖于环境变量的代码进行单元测试时,我们可能希望为它们提供特定的值,作为测试实现的一部分。

Java doesn’t allow us to edit the environment variables, but there are workarounds we can use, and some libraries which can help us.

Java 不允许我们编辑环境变量,但我们可以使用一些变通方法,还有一些库可以帮助我们。

In this tutorial, we’ll look at the challenges of depending on environment variables in unit tests, how Java has made this even harder in recent versions, and the JUnit Pioneer, System Stubs, System Lambda and System Rules libraries. We’ll look at this for JUnit 4, JUnit 5 and TestNG.

在本教程中,我们将探讨在单元测试中依赖环境变量所面临的挑战、Java 如何在最近的版本中使这一问题变得更加困难,以及 JUnit Pioneer系统存根系统 Lambda 和系统规则 库。我们将针对 JUnit 4JUnit 5TestNG进行研究。

2. The Challenge of Changing Environment Variables

2.不断变化的环境变量带来的挑战

In other languages, such as JavaScript, we can very easily modify the environment in a test:

在 JavaScript 等其他语言中,我们可以非常容易地修改测试环境:

beforeEach(() => {
   process.env.MY_VARIABLE = 'set';
});

Java is a lot more strict. In Java, the environment variable map is immutable. It’s an unmodifiable Map that is initialized when the JVM starts. While there are good reasons for this, we may still wish to control our environment at test time.

Java 要严格得多。在 Java 中,环境变量 map 是不可变的。它是一个不可修改的 Map ,在 JVM 启动时被初始化。虽然这样做有很好的理由,但我们可能仍然希望在测试时控制环境。

2.1. Why the Environment Is Immutable

2.1.为什么环境是不可改变的

With the normal execution of Java programs, it could be potentially chaotic if something as global as the runtime environment config could be modified. This is especially risky when multiple threads are involved. For example, one thread might be modifying the environment at the same time as another, launching a process with that environment, and any conflicting settings may interact in unexpected ways.

在 Java 程序正常执行的情况下,如果像运行时环境配置这样的全局配置被修改,可能会造成混乱。当涉及多个线程时,这种情况尤其危险。例如,一个线程在修改环境的同时,另一个线程可能会使用该环境启动一个进程,任何冲突的设置都可能以意想不到的方式相互作用。

The designers of Java have, therefore, kept the global values in the environment variables map safe. In contrast, system properties are easily changed at runtime.

因此,Java 的设计者将环境变量映射中的全局值保持安全。相反,系统属性在运行时很容易更改

2.2. Working Around the Unmodifiable Map

2.2.绕过不可修改地图

There is a workaround for the immutable environment variables Map object. Despite its read only UnmodifiableMap type, we can break encapsulation and access an internal field using reflection:

对于不可变环境变量 Map 对象,有一种变通方法。尽管 UnmodifiableMap 类型是只读的,但我们可以打破封装,使用 reflection 访问内部字段:

Class<?> classOfMap = System.getenv().getClass();
Field field = classOfMap.getDeclaredField("m");
field.setAccessible(true);
Map<String, String> writeableEnvironmentVariables = (Map<String, String>)field.get(System.getenv());

The field m inside the UnmodifiableMap wrapper object is a mutable Map that we can change:

UnmodifiableMap 封装对象中的 m 字段是一个可变的 Map 字段,我们可以更改它:

writeableEnvironmentVariables.put("baeldung", "has set an environment variable");

assertThat(System.getenv("baeldung")).isEqualTo("has set an environment variable");

In practice, on Windows, there’s an alternative implementation of ProcessEnvironment that also accounts for case-insensitive environment variables, so libraries that use the above technique have to account for this too. However, in principle, this is how we can work around the immutable environment variables Map.

实际上,在 Windows 上,ProcessEnvironment 有另一种实现,它也会考虑不区分大小写的环境变量,因此使用上述技术的库也必须考虑这一点。不过,原则上,我们可以通过这种方式绕过不可变的环境变量 Map

After JDK 16, the module system became much more protective of the internals of the JDK, and using this reflective access has become more difficult.

在 JDK 16 之后,模块系统对 JDK 内部的保护变得更加严格,使用这种反射式访问变得更加困难

2.3. When Reflective Access Doesn’t Work

2.3.当反思性访问不起作用时

The Java module system has disabled reflective modification of its core internals by default since JDK 17. These are considered unsafe practices which could lead to runtime errors if the internals changed in the future.

自 JDK 17 开始,Java 模块系统默认禁用了对其核心内部结构的反射式修改。这些被认为是不安全的做法,如果将来内部结构发生变化,可能会导致运行时错误。

We may receive an error like this:

我们可能会收到类似这样的错误:

Unable to make field private static final java.util.HashMap java.lang.ProcessEnvironment.theEnvironment accessible: 
  module java.base does not "opens java.lang" to unnamed module @fdefd3f

This is an indication that the Java module system is preventing the use of reflection. This can be fixed by adding some extra command line parameters to our test runner config in pom.xml to use >–add-opens to permit this reflective access:

这表明 Java 模块系统阻止了反射的使用。要解决这个问题,可以在 pom.xml 中的测试运行器配置中添加一些额外的命令行参数,使用 >-add-opens 来允许这种反射访问:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <argLine>
            --add-opens java.base/java.util=ALL-UNNAMED
            --add-opens java.base/java.lang=ALL-UNNAMED
        </argLine>
    </configuration>
</plugin>

This workaround allows us to write code and use tools that break encapsulation via reflection. However, we may wish to avoid this, as the opening of these modules may allow for unsafe coding practices that work at test time but unexpectedly fail at runtime. We can instead choose tooling that doesn’t require this workaround.

这种变通方法允许我们编写代码和使用工具,通过反射打破封装。不过,我们可能希望避免这样做,因为这些模块的开放可能会导致不安全的编码实践,即在测试时有效,但在运行时却意外失效。我们可以选择不需要这种变通方法的工具。

2.4. Why We Need to Set Environment Variables Programmatically

2.4.为什么需要通过编程设置环境变量

It’s possible for our unit tests to be run with environment variables set by the test runner. This may be our first preference if we have a global configuration that applies across the entire test suite.

单元测试可以使用测试运行器设置的环境变量来运行。如果我们有一个适用于整个测试套件的全局配置,这可能是我们的首选。

We can achieve this by adding an environment variable to our surefire configuration in our pom.xml:

我们可以通过在 pom.xml 中的 surefire 配置中添加一个环境变量来实现这一点:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <environmentVariables>
            <SET_BY_SUREFIRE>YES</SET_BY_SUREFIRE>
        </environmentVariables>
    </configuration>
</plugin>

This variable is then visible to our tests:

然后,我们的测试就可以看到这个变量:

assertThat(System.getenv("SET_BY_SUREFIRE")).isEqualTo("YES");

However, we may have code that operates differently depending on differing environment variable settings. We might prefer to be able to test all variations of this behaviour, using different values of an environment variable in different test cases.

但是,我们的代码可能会根据不同的环境变量设置进行不同的操作。我们可能更希望能够测试这种行为的所有变化,在不同的测试用例中使用环境变量的不同值。

Similarly, we may have values at test time that aren’t predictable at coding time. A good example of this is the port where we’re running a WireMock or test database in a docker container.

同样,我们可能会在测试时获得编码时无法预测的值。一个很好的例子就是我们在一个 docker 容器中运行 WireMock 测试数据库

2.5. Getting the Right Help From Test Libraries

2.5.从测试库中获取正确帮助

There are several test libraries that can help us set environment variables at test time. Each has its own level of compatibility with different test frameworks, and JDK versions.

有几个测试库可以帮助我们在测试时设置环境变量。每个库都与不同的测试框架和 JDK 版本有一定的兼容性。

We can choose the right library based on our preferred workflow, whether the environment variable’s value is known ahead of time, and which JDK version we’re planning to use.

我们可以根据自己偏好的工作流程、环境变量的值是否提前知道以及计划使用的 JDK 版本来选择合适的库。

We should note that all of these libraries cover more than just environment variables. They all use the approach of capturing the current environment before they make changes and returning the environment back to how it was after the test is complete.

我们需要注意的是,所有这些库涵盖的不仅仅是环境变量。它们都采用了一种方法,即在更改之前捕捉当前环境,并在测试完成后将环境恢复原状。

3. Setting Environment Variables With JUnit Pioneer

3.使用 JUnit 先锋设置环境变量

JUnit Pioneer is a set of extensions for JUnit 5. It offers an annotation-based way to set and clear environment variables.

JUnit Pioneer 是 JUnit 5 的扩展集。它提供了一种基于注解的设置和清除环境变量的方法。

We can add it with the junit-pioneer dependency:

我们可以通过 junit-pioneer 依赖关系来添加它:

<dependency>
    <groupId>org.junit-pioneer</groupId>
    <artifactId>junit-pioneer</artifactId>
    <version>2.1.0</version>
    <scope>test</scope>
</dependency>

3.1. Using the SetEnvironmentVariable Annotation

3.1.使用 SetEnvironmentVariable 注解

We can annotate a test class or method with the SetEnvironmentVariable annotation, and our test code operates with that value set in the environment:

我们可以使用 SetEnvironmentVariable 注解对测试类或方法进行注解,这样我们的测试代码就可以在环境中使用该值进行操作:

@SetEnvironmentVariable(key = "pioneer", value = "is pioneering")
class EnvironmentVariablesSetByJUnitPioneerUnitTest {

}

We should note that the key and value must be known at compile time.

我们应该注意到,keyvalue 必须在编译时已知。

Our test can then use the environment variable:

这样,我们的测试就可以使用环境变量了:

@Test
void variableCanBeRead() {
    assertThat(System.getenv("pioneer")).isEqualTo("is pioneering");
}

We can use the @SetEnvironmentVariable annotation multiple times to set multiple variables.

我们可以多次使用 @SetEnvironmentVariable 注解来设置多个变量。

3.2. Clearing an Environment Variable

3.2.清除环境变量

We may also wish to clear system-provided environment variables or even some that were set at class level for some specific tests:

我们可能还希望清除系统提供的环境变量,甚至是为某些特定测试在类级设置的环境变量:

@ClearEnvironmentVariable(key = "pioneer")
@Test
void givenEnvironmentVariableIsClear_thenItIsNotSet() {
    assertThat(System.getenv("pioneer")).isNull();
}

3.3. Limitations of JUnit Pioneer

3.3.JUnit 先锋的局限性

JUnit Pioneer can only be used with JUnit 5. It uses reflection, so it requires a version of Java from 16 or below, or for the add-opens workaround to be employed.

JUnit Pioneer 只能与 JUnit 5 配合使用。 它使用反射,因此需要 16 或更低版本的 Java,或使用 add-opens 解决办法。

4. Setting Environment Variables With System Stubs

4.使用系统存根设置环境变量

System Stubs has test support for JUnit 4, JUnit 5 and TestNG. Like its predecessor, System Lambda, it may also be used independently in the body of any test code in any framework. System Stubs is compatible with all versions of the JDK from 11 onwards.

System Stubs 支持 JUnit 4、JUnit 5 和 TestNG 测试。与它的前身 System Lambda 一样,它也可以在任何框架的任何测试代码正文中独立使用。System Stubs 兼容 JDK 11 及以后的所有版本

4.1. Setting Environment Variables in JUnit 5

4.1.在 JUnit 5 中设置环境变量

For this we need the System Stubs JUnit 5 dependency:

为此,我们需要 System Stubs JUnit 5 依赖项:

<dependency>
    <groupId>uk.org.webcompere</groupId>
    <artifactId>system-stubs-jupiter</artifactId>
    <version>2.1.3</version>
    <scope>test</scope>
</dependency>

First, we need to add the extension to our test class:

首先,我们需要在测试类中添加扩展:

@ExtendWith(SystemStubsExtension.class)
class EnvironmentVariablesUnitTest {
}

We can initialize an EnvironmentVariables stub object as a field of the test class with the environment variables we wish to use:

我们可以将一个 EnvironmentVariables 存根对象初始化为测试类的一个字段,其中包含我们希望使用的环境变量:

@SystemStub
private EnvironmentVariables environment = new EnvironmentVariables("MY VARIABLE", "is set");

Notably, we must annotate the object with @SystemStub so the extension knows how to work with it.

值得注意的是,我们必须用 @SystemStub 对对象进行注解,以便扩展知道如何使用它。

The SystemStubsExtension then activates this alternative environment during a test and clears it up afterward. During the test, the EnvironmentVariables object can also be modified and calls to System.getenv() receive the latest configuration.

然后,SystemStubsExtension 将在测试期间激活该替代环境,并在测试结束后将其清除。在测试过程中,EnvironmentVariables 对象也可以被修改,调用 System.getenv() 会收到最新的配置。

Let’s also look at a more complex situation where we wish to set an environment variable with a value only known at test initialization time. In this case, as we’re going to provide a value in our beforeEach() method, we don’t need to create an instance of the object in our initializer list:

我们还将看到一种更复杂的情况,即我们希望为环境变量设置一个只有在测试初始化时才知道的值在这种情况下,由于我们将在 beforeEach() 方法中提供一个值,因此无需在初始化器列表中创建对象的实例:

@SystemStub
private EnvironmentVariables environmentVariables;

By the time JUnit calls our beforeEach(), the extension has created the object for us, and we can use it to set the environment variables we need:

当 JUnit 调用我们的 beforeEach() 时,扩展已经为我们创建了对象,我们可以使用它来设置我们需要的环境变量:

@BeforeEach
void beforeEach() {
    environmentVariables.set("systemstubs", "creates stub objects");
}

When our test is executed, the environment variables will be active:

执行测试时,环境变量将处于激活状态:

@Test
void givenEnvironmentVariableHasBeenSet_thenCanReadIt() {
    assertThat(System.getenv("systemstubs")).isEqualTo("creates stub objects");
}

After the test method has completed, the environment variables return to the state they were in before modification.

测试方法完成后,环境变量会恢复到修改前的状态。

4.2. Setting Environment Variables in JUnit 4

4.2.在 JUnit 4 中设置环境变量

For this, we need the System Stubs JUnit 4 dependency:

为此,我们需要 System Stubs JUnit 4 依赖项:

<dependency>
    <groupId>uk.org.webcompere</groupId>
    <artifactId>system-stubs-junit4</artifactId>
    <version>2.1.3</version>
    <scope>test</scope>
</dependency>

System Stubs provides a JUnit 4 rule. We add this as a field of our test class:

系统存根提供了一条 JUnit 4 规则。我们将其添加为测试类的一个字段:

@Rule
public EnvironmentVariablesRule environmentVariablesRule =
  new EnvironmentVariablesRule("system stubs", "initializes variable");

Here we’ve initialized it with an environment variable. We can also call set() on the rule to modify the variables during our tests, or within our @Before method.

在这里,我们使用环境变量对其进行了初始化。我们还可以在测试过程中或在 @Before 方法中调用规则上的 set() 来修改变量。

Once the test is running, the environment variable is active:

测试运行后,环境变量就会激活:

@Test
public void canReadVariable() {
    assertThat(System.getenv("system stubs")).isEqualTo("initializes variable");
}

4.3. Setting Environment Variables in TestNG

4.3.在 TestNG 中设置环境变量

For this we need the System Stubs TestNG dependency:

为此,我们需要 System Stubs TestNG 依赖项:

<dependency>
    <groupId>uk.org.webcompere</groupId>
    <artifactId>system-stubs-testng</artifactId>
    <version>2.1.3</version>
    <scope>test</scope>
</dependency>

This provides a TestNG listener that works like the JUnit 5 solution above.

这将提供一个 TestNG 监听器,其工作原理与上述 JUnit 5 解决方案类似。

We add the listener to our test class:

我们将监听器添加到测试类中:

@Listeners(SystemStubsListener.class)
public class EnvironmentVariablesTestNGUnitTest {
}

Then we add an EnvironmentVariables field annotated with @SystemStub:

然后,我们添加一个注释为 @SystemStubEnvironmentVariables 字段:

@SystemStub
private EnvironmentVariables setEnvironment;

Then our beforeAll() method can initialise some variables:

然后,我们的 beforeAll() 方法可以初始化一些变量:

@BeforeClass
public void beforeAll() {
    setEnvironment.set("testng", "has environment variables");
}

And our test method can use them:

而我们的测试方法可以使用它们:

@Test
public void givenEnvironmentVariableWasSet_thenItCanBeRead() {
    assertThat(System.getenv("testng")).isEqualTo("has environment variables");
}

4.4. System Stubs Without a Test Framework

4.4.无测试框架的系统存根

System Stubs was originally based on the codebase of System Lambda, which came with techniques that could only be used inside a single test method. This meant that the choice of test framework was completely open.

System Stubs 最初是基于 System Lambda 的代码库,其附带的技术只能在单个测试方法中使用。这意味着测试框架的选择是完全开放的。

System Stubs Core, therefore, can be used to set environment variables anywhere in a JUnit test method.

因此,System Stubs Core 可用于在 JUnit 测试方法的任何地方设置环境变量。

First, let’s get the system-stubs-core dependency:

首先,让我们获取 system-stubs-core 依赖项:

<dependency>
    <groupId>uk.org.webcompere</groupId>
    <artifactId>system-stubs-core</artifactId>
    <version>2.1.3</version>
    <scope>test</scope>
</dependency>

Now, in one of our test methods, we can surround the test code with a construct that temporarily sets some environment variables. First we need to statically import from SystemStubs:

现在,在我们的一个测试方法中,我们可以用一个临时设置一些环境变量的结构来包围测试代码。首先,我们需要静态导入 SystemStubs

import static uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariables;

Then we can use the withEnvironmentVariables() method to wrap our test code:

然后,我们可以使用 withEnvironmentVariables() 方法来封装我们的测试代码:

@Test
void useEnvironmentVariables() throws Exception {
    withEnvironmentVariables("system stubs", "in test")
      .execute(() -> {
          assertThat(System.getenv("system stubs"))
            .isEqualTo("in test");
      });
}

Here we can see that the assertThat() call is an operation on an environment that has the variable set in it. Outside of the closure called by execute(), the environment variables are unaffected.

在这里,我们可以看到,assertThat() 调用是对环境中已设置变量的操作。在 execute() 调用的闭包之外,环境变量不受影响。

We should note that this technique requires our test to have throws Exception on our test method since the execute() function has to cope with closures that may make calls to methods with checked exceptions.

我们应该注意的是,这种技术要求我们的测试在测试方法中设置 throws Exception,因为 execute() 函数必须处理可能调用已检查异常的方法的闭包。

This technique also requires each test to set its own environment and doesn’t work well if we’re trying to work with test objects with a lifecycle larger than a single test, for example, a Spring Context.

这种技术还要求每个测试设置自己的环境,如果我们要处理生命周期大于单个测试的测试对象(例如 Spring Context),这种技术就不能很好地发挥作用。

System Stubs allows each of its stub objects to be set up and torn down independently of a test framework. So, we could use the beforeAll() and afterAll() methods of a test class to manipulate our EnvironmentVariables object:

System Stubs 允许独立于测试框架设置和删除每个存根对象。因此,我们可以使用测试类的 beforeAll()afterAll() 方法来操作我们的 EnvironmentVariables 对象:

private static EnvironmentVariables environmentVariables = new EnvironmentVariables();

@BeforeAll
static void beforeAll() throws Exception {
    environmentVariables.set("system stubs", "in test");
    environmentVariables.setup();
}

@AfterAll
static void afterAll() throws Exception {
    environmentVariables.teardown();
}

The benefit of the test framework extensions, however, is that we can avoid this sort of boilerplate, as they execute these basics for us.

不过,测试框架扩展的好处是,我们可以避免这类模板,因为它们会为我们执行这些基本操作。

4.5. Limitations of System Stubs

4.5.系统存根的局限性

The TestNG capability of System Stubs is only available in version 2.1+ releases, which are limited to Java 11 onwards.

系统存根的 TestNG 功能仅在 2.1+ 版本中可用,该版本仅限于 Java 11 及以后的版本。

In its version 2 release train, System Stubs deviated from the common reflection-based techniques described earlier. It now uses ByteBuddy to intercept environment variable calls. However, if a project uses a lower version of the JDK than 11, then there’s also no need to use these later versions.

在第 2 版的发布列车中,System Stubs 偏离了前面所述的基于反射的常用技术。它现在使用 ByteBuddy 来拦截环境变量调用。不过,如果项目使用的 JDK 版本低于 11,那么也就没有必要使用这些后续版本。

System Stubs version 1 provides compatibility with JDK 8 to JDK 16.

系统存根版本 1 兼容 JDK 8 至 JDK 16。

5. System Rules and System Lambda

5.系统规则和系统 Lambda

One of the longest-standing test libraries for environment variables, System Rules provided a JUnit 4 solution to setting environment variables, and its author replaced it with System Lambda to provide a test-framework agnostic approach. They’re based on the same core techniques for substituting environment variables at test time.

作为历史最悠久的环境变量测试库之一,System Rules 为设置环境变量提供了 JUnit 4 解决方案,其作者将其替换为 System Lambda,以提供一种与测试框架无关的方法。它们基于相同的核心技术,可在测试时替换环境变量。

5.1. Set Environment Variables With System Rules

5.1.使用系统规则设置环境变量

First we need the system-rules dependency:

首先,我们需要 system-rules 依赖项:

<dependency>
    <groupId>com.github.stefanbirkner</groupId>
    <artifactId>system-rules</artifactId>
    <version>1.19.0</version>
    <scope>test</scope>
</dependency>

Then we add the rule to our JUnit 4 test class:

然后,我们将该规则添加到 JUnit 4 测试类中:

@Rule
public EnvironmentVariables environmentVariablesRule = new EnvironmentVariables();

We can set up the values in our @Before method:

我们可以在 @Before 方法中设置这些值:

@Before
public void before() {
    environmentVariablesRule.set("system rules", "works");
}

And access the correct environment in our test method:

并在我们的测试方法中访问正确的环境:

@Test
public void givenEnvironmentVariable_thenCanReadIt() {
    assertThat(System.getenv("system rules")).isEqualTo("works");
}

The rule object – environmentVariablesRule – allows us to set environment variables immediately within the test method too.

规则对象–environmentVariablesRule–也允许我们在测试方法中立即设置环境变量。

5.2. Set Environment Variables With System Lambda

5.2.使用系统 Lambda 设置环境变量

For this we need the system-lambda dependency:

为此,我们需要 system-lambda 依赖关系:

<dependency>
    <groupId>com.github.stefanbirkner</groupId>
    <artifactId>system-lambda</artifactId>
    <version>1.2.1</version>
    <scope>test</scope>
</dependency>

As already demonstrated in the System Stubs solution, we can put the code that depends on the environment within a closure in our test. For this we should statically import SystemLambda:

正如在 System Stubs 解决方案中已经演示过的,我们可以将依赖于环境的代码放在测试的闭包中。为此,我们应静态导入 SystemLambda

import static com.github.stefanbirkner.systemlambda.SystemLambda.withEnvironmentVariable;

Then we can write the test:

然后,我们就可以编写测试了:

@Test
void enviromentVariableIsSet() throws Exception {
    withEnvironmentVariable("system lambda", "in test")
      .execute(() -> {
          assertThat(System.getenv("system lambda"))
            .isEqualTo("in test");
      });
}

5.3. Limitations of System Rules and System Lambda

5.3.系统规则和系统 Lambda 的限制

While these are both mature and broad libraries, they cannot be used for manipulating environment variables in JDK 17 and beyond.

虽然这些都是成熟而广泛的库,但在 JDK 17 及以后的版本中,它们不能用于操作环境变量。

System Rules depends heavily on JUnit 4. We cannot use System Lambda for setting test-fixture wide environment variables, so it cannot help us with Spring context initialization.

系统规则在很大程度上依赖于 JUnit 4。我们无法使用 System Lambda 来设置测试夹具环境变量,因此 它无法帮助我们进行 Spring 上下文初始化

6. Avoid Mocking Environment Variables

6.避免模拟环境变量

While we have discussed a number of ways to modify environment variables at test time, it may be worth considering whether this is necessary, or even beneficial.

虽然我们已经讨论过许多在测试时修改环境变量的方法,但这是否必要,甚至是否有益,也许值得考虑。

6.1. Maybe It Is Too Risky

6.1.也许风险太大

As we’ve seen with each of the solutions above, changing the environment variables at runtime is not straightforward. In cases where there is multi-threaded code, it can be even more tricky. If multiple test fixtures are running in the same JVM in parallel, perhaps with JUnit 5’s concurrency features, then there is a risk that different tests may be trying to take control of the environment at the same time in a contradictory way.

正如我们在上述每种解决方案中所看到的,在运行时更改环境变量并不简单。在有多线程代码的情况下,这可能更加棘手。如果多个测试夹具在同一 JVM 中并行运行(可能使用了 JUnit 5 的并发功能),那么不同的测试就有可能试图以相互矛盾的方式同时控制环境。

Though some of the testing libraries above may not crash when used simultaneously by multiple threads, it would be hard to predict how the environment variables might be set from one moment to the next. Even worse, it’s possible that one thread might capture another test’s temporary environment variables as though they were the correct state to leave the system in when the tests are all finished.

虽然上述某些测试库在多个线程同时使用时可能不会崩溃,但很难预测环境变量在不同时刻的设置情况。更糟糕的是,一个线程可能会捕获另一个测试的临时环境变量,就好像它们是测试全部结束后系统的正确状态。

As an example from another test library, when Mockito mocks static methods, it limits that to the current thread, since such mocking globals can break concurrent tests. As such, modifying environment variables encounters exactly the same risks. One test can affect the entire global state of the JVM and cause side effects elsewhere.

以另一个测试库为例,当Mockito模拟静态方法时,它会将模拟限制在当前线程内,因为这种模拟全局会破坏并发测试。因此,修改环境变量也会遇到完全相同的风险。一个测试可能会影响 JVM 的整个全局状态,并在其他地方产生副作用。

Similarly, if we’re running code that we can only control through environment variables, it can be very difficult to test, and surely we could avoid that by design?

同样,如果我们运行的代码只能通过环境变量来控制,那么测试起来就会非常困难,而我们当然可以通过设计来避免这种情况。

6.2. Use Dependency Injection

6.2.使用依赖注入

It’s easier to test a system that receives all its inputs at construction, than one that pulls its inputs from system resources.

与从系统资源中获取输入的系统相比,在构建时接收所有输入的系统更容易进行测试。

Dependency injection containers such as Spring allow us to build objects that are much easier to test in isolation of the runtime.

依赖注入容器(如 Spring)允许我们构建更易于在运行时进行测试的对象。

We should also note that Spring will allow us to use system properties in place of environment variables to set any of its property values. Each of the tools we’ve discussed in this article also supports setting and resetting system properties at test time.

我们还应该注意到,Spring 允许我们使用系统属性代替环境变量来设置其任何属性值。我们在本文中讨论的每种工具也都支持在测试时设置和重置系统属性。

6.3. Use an Abstraction

6.3.使用抽象

If a module must pull environment variables, perhaps it should not depend directly on System.getenv() but could instead use an environment variable reader interface:

如果模块必须读取环境变量,也许它不应直接依赖 System.getenv() 而应使用环境变量读取器接口:

@FunctionalInterface
interface GetEnv {
    String get(String name);
}

The system code could then have an object of this injected via constructor:

然后,系统代码就可以通过构造函数注入该对象:

public class ReadsEnvironment {
    private GetEnv getEnv;

    public ReadsEnvironment(GetEnv getEnv) {
        this.getEnv = getEnv;
    }

    public String whatOs() {
        return getEnv.get("OS");
    }
}

While at runtime, we might instantiate it with System::getenv, at test time we could pass in our own alternative environment:

在运行时,我们可以使用 System::getenv 将其实例化,而在测试时,我们可以传入自己的替代环境:

Map<String, String> fakeEnv = new HashMap<>();
fakeEnv.put("OS", "MacDowsNix");

ReadsEnvironment reader = new ReadsEnvironment(fakeEnv::get);
assertThat(reader.whatOs()).isEqualTo("MacDowsNix");

However, these alternatives to environment variables may seem very heavy, and leave us wishing Java gave us the control we saw with the JavaScript example earlier. Similarly, we cannot control code written by others, which may depend on environment variables.

但是,这些环境变量的替代方案可能看起来非常繁琐,让我们希望 Java 能提供我们在前面的 JavaScript 示例中看到的控制功能。同样,我们无法控制他人编写的代码,这些代码可能依赖于环境变量。

Therefore, it seems inevitable that we may still encounter cases where we want to be able to control some environment variables dynamically at test time.

因此,我们可能仍会遇到希望在测试时动态控制某些环境变量的情况,这似乎是不可避免的。

7. Conclusion

7.结论

In this article, we’ve looked at the options for setting environment variables at test time. We saw that this becomes more difficult to do when we need to be able to make those variables flexible at runtime, and available to JDK 17 and beyond.

在本文中,我们了解了在测试时设置环境变量的选项。我们发现,当我们需要在运行时灵活设置这些变量,并使其适用于 JDK 17 及更高版本时,设置环境变量就变得更加困难。

Then, we discussed whether we can avoid the issue altogether if we write our production code differently. We considered the risks associated with modifying the environment variables at test time, especially with concurrent tests.

然后,我们讨论了如果以不同的方式编写生产代码,是否可以完全避免这一问题。我们考虑了在测试时修改环境变量的风险,尤其是并发测试。

We also explored four of the most popular libraries for setting environment variables at test time: JUnit Pioneer, System Stubs, System Rules, and System Lambda. Each of these offers a different way to solve the problem, with differing compatibility across JDK versions and test frameworks.

我们还探讨了用于在测试时设置环境变量的四个最流行的库:JUnit Pioneer、System Stubs、System Rules 和 System Lambda。它们各自提供了不同的方法来解决问题,在不同的 JDK 版本和测试框架中具有不同的兼容性。

As always the example code for this article is available over on GitHub.

与往常一样,本文的示例代码可在 GitHub 上获取。