如何使用 JUnit 测试依赖于环境变量的代码?

我有一段 Java 代码,它使用了一个环境变量,代码的行为取决于这个变量的值。我想用不同的环境变量值来测试这个代码。我如何在 JUnit 中做到这一点?

我一般都看过 在 Java 中设置环境变量的一些方法,但是我对它的单元测试方面更感兴趣,特别是考虑到测试不应该相互干扰。

253879 次浏览

把 Java 代码从环境变量中解耦出来,提供一个更抽象的变量阅读器,你可以通过一个 Environment mentVariableReader 来测试从中读取的代码。

然后在您的测试中,您可以给出提供测试值的变量阅读器的不同实现。

依赖注入可以帮上忙。

通常的解决方案是创建一个类来管理对这个环境变量的访问,然后您可以在测试类中模拟这个环境变量。

public class Environment {
public String getVariable() {
return System.getenv(); // or whatever
}
}


public class ServiceTest {
private static class MockEnvironment {
public String getVariable() {
return "foobar";
}
}


@Test public void testService() {
service.doSomething(new MockEnvironment());
}
}

然后,被测试的类使用 Environment 类获取环境变量,而不是直接从 System.getenv ()获取。

问题 如何从 Java 设置环境变量?这个答案提供了一种方法来修改 System.getenv ()中的(不可修改的) Map。因此,虽然它不会真正改变操作系统环境变量的值,但它可以用于单元测试,因为它确实改变了 System.getenv将返回的内容。

您可以使用 setup ()方法来声明 env 的不同值。常数中的变量。然后在用于测试不同场景的测试方法中使用这些常量。

如果你想在 Java 中检索关于环境变量的信息,你可以调用方法: System.getenv();。作为属性,此方法返回一个 Map,其中包含变量名作为键,变量值作为 Map 值。这里有一个例子:

    import java.util.Map;


public class EnvMap {
public static void main (String[] args) {
Map<String, String> env = System.getenv();
for (String envName : env.keySet()) {
System.out.format("%s=%s%n", envName, env.get(envName));
}
}
}

方法 getEnv()也可以接受一个参数。例如:

String myvalue = System.getEnv("MY_VARIABLE");

对于测试,我会这样做:

public class Environment {
public static String getVariable(String variable) {
return  System.getenv(variable);
}


@Test
public class EnvVariableTest {


@Test testVariable1(){
String value = Environment.getVariable("MY_VARIABLE1");
doSometest(value);
}


@Test testVariable2(){
String value2 = Environment.getVariable("MY_VARIABLE2");
doSometest(value);
}
}

我认为最干净的方法是使用 Mockito.py ()。它比创建一个单独的类来模拟和传递要轻量级一些。

移动你的环境变量获取到另一种方法:

@VisibleForTesting
String getEnvironmentVariable(String envVar) {
return System.getenv(envVar);
}

现在在单元测试中这样做:

@Test
public void test() {
ClassToTest classToTest = new ClassToTest();
ClassToTest classToTestSpy = Mockito.spy(classToTest);
Mockito.when(classToTestSpy.getEnvironmentVariable("key")).thenReturn("value");
// Now test the method that uses getEnvironmentVariable
assertEquals("changedvalue", classToTestSpy.methodToTest());
}

Lambda 系统有一个用于设置环境变量的方法 withEnvironmentVariable

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


public void EnvironmentVariablesTest {
@Test
public void setEnvironmentVariable() {
String value = withEnvironmentVariable("name", "value")
.execute(() -> System.getenv("name"));
assertEquals("value", value);
}
}

对于 Java5到7,库 系统规则有一个名为 EnvironmentVariables的 JUnit 规则。

import org.junit.contrib.java.lang.system.EnvironmentVariables;


public class EnvironmentVariablesTest {
@Rule
public final EnvironmentVariables environmentVariables
= new EnvironmentVariables();


@Test
public void setEnvironmentVariable() {
environmentVariables.set("name", "value");
assertEquals("value", System.getenv("name"));
}
}

完全公开: 我是这两个库的作者。

在类似的情况下,我不得不编写依赖于 环境变量测试案例,我试着这样做:

  1. 我按照 Stefan Birkner的建议选择了 系统规则。用法很简单。但很快,我就发现他的行为很古怪。在一次运行中,它起作用了,但在下一次运行中,它失败了。但是在我的案例中,我使用了一些依赖于 JUnit 3的罐子。所以我跳过了 系统规则。你可以在这里找到更多关于它的 @ Rule 注释在 JUnit 中使用 TestSuite 时不起作用
  2. 接下来,我尝试通过 爪哇咖啡提供的 流程生成器类创建 环境变量。这里通过 Java 代码我们可以创建一个环境变量,但是你需要知道的是我不知道的 程序或者 程序的名字。此外,它还为子进程创建了环境变量,而不是为主进程创建。

我用上面两种方法浪费了一天时间,但是没有用。然后 玛文来救我了。我们可以设置 环境变数系统属性通过 Maven POM文件,我认为最好的办法做 单位测试玛文为基础的项目。下面是我在 POM文件中创建的条目。

    <build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<PropertyName1>PropertyValue1</PropertyName1>
<PropertyName2>PropertyValue2</PropertyName2>
</systemPropertyVariables>
<environmentVariables>
<EnvironmentVariable1>EnvironmentVariableValue1</EnvironmentVariable1>
<EnvironmentVariable2>EnvironmentVariableValue2</EnvironmentVariable2>
</environmentVariables>
</configuration>
</plugin>
</plugins>
</build>

在这个更改之后,我再次运行 测试案例,突然所有的工作都像预期的那样。对于读者的信息,我探讨了这种方法在 Maven 3. x,所以我不知道在 美芬2. x

我认为这还没有被提及,但是你也可以使用 强大的力量:

给出:

package com.foo.service.impl;


public class FooServiceImpl {


public void doSomeFooStuff() {
System.getenv("FOO_VAR_1");
System.getenv("FOO_VAR_2");
System.getenv("FOO_VAR_3");


// Do the other Foo stuff
}
}

你可以这样做:

package com.foo.service.impl;


import static org.mockito.Mockito.when;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.verifyStatic;


import org.junit.Beforea;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;


@RunWith(PowerMockRunner.class)
@PrepareForTest(FooServiceImpl.class)
public class FooServiceImpTest {


@InjectMocks
private FooServiceImpl service;


@Before
public void setUp() {
MockitoAnnotations.initMocks(this);


mockStatic(System.class);  // Powermock can mock static and private methods


when(System.getenv("FOO_VAR_1")).thenReturn("test-foo-var-1");
when(System.getenv("FOO_VAR_2")).thenReturn("test-foo-var-2");
when(System.getenv("FOO_VAR_3")).thenReturn("test-foo-var-3");
}


@Test
public void testSomeFooStuff() {
// Test
service.doSomeFooStuff();


verifyStatic();
System.getenv("FOO_VAR_1");
verifyStatic();
System.getenv("FOO_VAR_2");
verifyStatic();
System.getenv("FOO_VAR_3");
}
}

我使用 System.getEnv ()来获取映射,并将其保存为一个字段,这样我就可以模仿它:

public class AAA {


Map<String, String> environmentVars;


public String readEnvironmentVar(String varName) {
if (environmentVars==null) environmentVars = System.getenv();
return environmentVars.get(varName);
}
}






public class AAATest {


@Test
public void test() {
aaa.environmentVars = new HashMap<String,String>();
aaa.environmentVars.put("NAME", "value");
assertEquals("value",aaa.readEnvironmentVar("NAME"));
}
}

希望问题解决了。我只是想告诉我的解决方案。

Map<String, String> env = System.getenv();
new MockUp<System>() {
@Mock
public String getenv(String name)
{
if (name.equalsIgnoreCase( "OUR_OWN_VARIABLE" )) {
return "true";
}
return env.get(name);
}
};

即使我认为 这个答案是最适合 Maven 项目的,它也可以通过反射来实现(在 爪哇8中测试) :

public class TestClass {
private static final Map<String, String> DEFAULTS = new HashMap<>(System.getenv());
private static Map<String, String> envMap;


@Test
public void aTest() {
assertEquals("6", System.getenv("NUMBER_OF_PROCESSORS"));
System.getenv().put("NUMBER_OF_PROCESSORS", "155");
assertEquals("155", System.getenv("NUMBER_OF_PROCESSORS"));
}


@Test
public void anotherTest() {
assertEquals("6", System.getenv("NUMBER_OF_PROCESSORS"));
System.getenv().put("NUMBER_OF_PROCESSORS", "77");
assertEquals("77", System.getenv("NUMBER_OF_PROCESSORS"));
}


/*
* Restore default variables for each test
*/
@BeforeEach
public void initEnvMap() {
envMap.clear();
envMap.putAll(DEFAULTS);
}


@BeforeAll
public static void accessFields() throws Exception {
envMap = new HashMap<>();
Class<?> clazz = Class.forName("java.lang.ProcessEnvironment");
Field theCaseInsensitiveEnvironmentField = clazz.getDeclaredField("theCaseInsensitiveEnvironment");
Field theUnmodifiableEnvironmentField = clazz.getDeclaredField("theUnmodifiableEnvironment");
removeStaticFinalAndSetValue(theCaseInsensitiveEnvironmentField, envMap);
removeStaticFinalAndSetValue(theUnmodifiableEnvironmentField, envMap);
}


private static void removeStaticFinalAndSetValue(Field field, Object value) throws Exception {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, value);
}
}

对于 JUnit4用户,Stefan Birkner建议的 Lambda 系统非常适合。

如果您正在使用 JUnit5,那么有一个 JUnit 先锋扩展包。它附带 @ClearEnvironmentVariable@SetEnvironmentVariable。来自 医生:

可以分别使用 @ClearEnvironmentVariable@SetEnvironmentVariable注释来清除,为测试执行设置环境变量的值。这两个注释都在测试方法和类级别上起作用,它们是可重复和可组合的。在执行注释方法之后,注释中提到的变量将恢复到它们的原始值,或者如果它们之前没有变量,那么它们将被清除。在测试过程中更改的其他环境变量将被还原为 没有

例如:

@Test
@ClearEnvironmentVariable(key = "SOME_VARIABLE")
@SetEnvironmentVariable(key = "ANOTHER_VARIABLE", value = "new value")
void test() {
assertNull(System.getenv("SOME_VARIABLE"));
assertEquals("new value", System.getenv("ANOTHER_VARIABLE"));
}

你可以用 Powermock来嘲笑这个电话,比如:

PowerMockito.mockStatic(System.class);
PowerMockito.when(System.getenv("MyEnvVariable")).thenReturn("DesiredValue");

您还可以使用以下命令模拟所有调用:

PowerMockito.mockStatic(System.class);
PowerMockito.when(System.getenv(Mockito.anyString())).thenReturn(envVariable);

https://github.com/webcompere/system-stubs/tree/master/system-stubs-jupiter—— system-lambda的分支——提供了 JUnit 5插件:

@ExtendWith(SystemStubsExtension.class)
class SomeTest {
@SystemStub
private EnvironmentVariables environmentVariables =
new EnvironmentVariables("name", "value");


@Test
void someTest() {
// environment is set here


// can set a new value into the environment too
environmentVariables.set("other", "value");


// tidy up happens at end of this test
}


}
可选的 https://junit-pioneer.org/要求在编译时知道环境变量值,上面的代码也支持这个设置 @BeforeAll中的环境变量,这意味着它可以很好地与 Testcontainers之类的东西互操作,这些东西可以设置子测试所需的一些资源。译注:

在上面的建议中,我们重点介绍了 在运行时传递变量、设置变量和清除变量的方法等。.?但是为了“从结构上”测试,我猜您想要针对不同的场景使用不同的测试套件?非常类似于当您想要运行您的“重型”集成测试构建时,而在大多数情况下您只是想要跳过它们。但是,你不会尝试“发明在运行时设置内容的方法”,而只是按照 说吧标准设置你想要的内容?过去,告诉 Maven 通过配置文件运行特定的测试需要做很多工作,如果你谷歌一下人们会建议通过 springboot 来做这件事(但是如果你没有把 springboot 怪兽拖进你的项目,那么“仅仅运行 JUnit”似乎是一个可怕的足迹,对吗.否则,它将意味着大量或多或少不方便的 POM XML 杂耍,这也是令人厌烦的,让我们只是说,“90年代移动”,不方便的仍然坚持使“从 XML 的弹簧豆”,炫耀你的 终极的600行 logback.XML 或诸如此类的... ?

现在,您可以只使用 Junit5(这个例子是为了 maven,在这里可以找到更多的详细信息 JUnit 5用户指南5)

 <dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.7.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

然后

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

然后在您最喜欢的实用程序库中创建一个简单漂亮的注释类,如

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@EnabledIfEnvironmentVariable(named = "MAVEN_CMD_LINE_ARGS", matches = "(.*)integration-testing(.*)")
public @interface IntegrationTest {}

因此,每当您的 cmdline 选项包含-PIntegration-testingfor instance 时,那么只有在那时,您的@IntegrationTest 注释的 test-class/method 才会触发。或者,如果您不想使用(和设置)特定的 maven 配置文件,而只是通过以下方法传入“触发器”系统属性

mvn <cmds> -DmySystemPop=mySystemPropValue

并调整您的注释接口以触发它(是的,还有一个@EnabledIfSystemProperty)。或者确保将 shell 设置为包含“您需要的任何内容”,或者如上所述,通过 POM XML 实际上经历了添加系统 env 的“痛苦”。

让你的代码 在内部在运行时摆弄 env 或者嘲弄 env,设置它,然后可能“清除”运行时 env,在执行过程中改变它自己,这看起来是一种糟糕的,甚至是危险的方法——很容易想象有人迟早会犯一个“隐藏的”内部错误,这个错误会被忽视一段时间,只是突然出现,然后在以后的生产中给你带来严重的影响。.?你通常更喜欢这样一种方法,即“给定输入”给出“预期输出”,这种方法随着时间的推移很容易掌握和维护,你的程序员同事只会“立即”看到它。

很长的“回答”,或者更确切地说是关于您为什么喜欢这种方法的意见(是的,起初我只是阅读了这个问题的标题,然后继续回答,即“如何使用 JUnit 测试依赖于环境变量的代码”)。

一个缓慢的,可靠的,老式的方法,总是工作在每个操作系统的每种语言(甚至语言之间)是写的“系统/环境”数据,你需要一个临时文本文件,读取它时,你需要它,然后删除它。当然,如果并行运行,那么需要文件的唯一名称,如果要在其中放入敏感信息,则需要对其进行加密。

很简单

添加以下依赖项

<!-- 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>

在你的测试中,你可以使用类似的东西:

@Rule
public EnvironmentVariablesRule environmentVariablesRule = new EnvironmentVariablesRule();


@Test
public void givenEnvironmentCanBeModified_whenSetEnvironment_thenItIsSet() {
// mock that the system contains an  environment variable "ENV_VAR" having value "value1"
environmentVariablesRule.set("ENV_VAR", "value1");


assertThat(System.getenv("ENV_VAR")).isEqualTo("value1");
}
更多详情请参阅 < br/> https://www.baeldung.com/java-system-stubs

您可以尝试将代码也依赖于属性:

public static String host() {
return firstNonBlank(getenv("HOST"), getProperty("host"), "localhost");
}

因此,在测试中,你可以很容易地添加系统属性,你的生产代码将在使用环境变量之前:

System.setProperty("HOST", "127.0.0.0");

在单元测试中使用环境变量模拟的简洁方法是借助于 @SystemStub,它是下面依赖项的一部分

testImplementation 'uk.org.webcompere:system-stubs-jupiter:2.0.1'
下面是对 第五小队设置的更改 在类

中添加以下内容
@ExtendWith(SystemStubsExtension.class)

现在使用

@SystemStub
private EnvironmentVariables environmentVariables;

现在,您可以通过在测试中设置所需的键/值来模拟环境变量的行为。

< p > 例如 environmentVariables.set("MY_ENV_VARIABLE", "MY_REQUIRED_VALUE"); 如果您的代码使用的是环境变量

,那么这种方法可以很好地工作
 System.getenv().getOrDefault("MY_ENV_VARIABLE", "false");

请注意,它不会模拟 System.getProperties()它只适用于 System.getenv()