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

215

我有一段使用环境变量的Java代码,代码的行为取决于这个变量的值。我想测试不同值的环境变量下的代码行为。在JUnit中如何实现呢?

我看到一些 Java 中设置环境变量的方法(链接),但我更关心单元测试方面的问题,特别是考虑到测试之间不应该互相干扰。


由于这是用于测试,因此“System Rules”单元测试规则可能是目前最好的答案。 - Atif
4
只针对那些在使用JUnit 5时对同样问题感兴趣的人:https://dev59.com/yVYN5IYBdhLWcg3w07Bx - Felipe Martins Melo
@FelipeMartinsMelo 这个问题是关于系统属性而不是环境变量的。这里有一个适用于环境变量的JUnit 5兼容解决方案:https://dev59.com/jmsz5IYBdhLWcg3wADS6#63494695 - beatngu13
这里有一个设置JUNIT环境变量的技巧:https://dev59.com/nnRC5IYBdhLWcg3wXP0C - Timur Sadykov
21个回答

255

这个名为System 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);
  }
}

对于Java 5到7,库 System Rules 具有名为 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
我正在使用@ClassRule,使用后是否需要重置或清除它?如果是,应该如何操作? - Mritunjay
这种方法仅适用于JUnit 4或更高版本。不建议在JUnit 3或更低版本中使用,也不建议混合使用JUnit 4和JUnit 3。 - RLD
2
你需要在项目中添加依赖项 com.github.stefanbirkner:system-rules。它可以在MavenCentral上获取。同时,导入 org.junit.contrib.java.lang.system.EnvironmentVariables 包。 - Jean Bob
2
这里是添加依赖项的说明:https://stefanbirkner.github.io/system-rules/download.html - Guilherme Garnier
1
Stefan,这听起来很棒 - 但这是否会改变测试代码的环境变量值?如果答案很明显,请原谅我,但是你的回答和SystemRules自述文件似乎都没有回答我的问题。 - CPerkins
显示剩余8条评论

135

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

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 class)获取环境变量,而不是直接从System.getenv()方法中获取。

27
我知道这个问题早已过时,但我想说这是正确的答案。被接受的答案鼓励了一种糟糕的设计,其中包含对System的隐藏依赖,而这个答案鼓励采用正确的设计方式,将System视为另一个应该被注入的依赖。 - Andrew
1
如果您不在意覆盖率达到100%,而更关心良好的设计和关注点分离,请寻找此答案。如果您真的想达到100%,请使用此答案和先前的答案来测试此服务。 - Abdullah Khaled

54
在类似这种情况下,我需要编写依赖于“环境变量”的“测试用例”,我尝试了以下方法:
  1. 根据Stefan Birkner的建议,我选择了System Rules。它的使用非常简单。但很快我发现它的行为不稳定。有时它能运行,下一次就会失败。我进行了调查,发现System Rules与JUnit 4或更高版本兼容良好。但在我的情况下,我使用了一些依赖于JUnit 3的Jars。因此我跳过了System Rules。更多信息可以在这里找到 @Rule annotation doesn't work while using TestSuite in JUnit
  2. 接下来,我尝试通过Java提供的Process Builder类创建环境变量。通过Java代码,我们可以创建一个环境变量,但需要知道进程程序名称,而我不知道。此外,它只为子进程创建环境变量,而不是主进程。
我浪费了一天时间尝试上述两种方法,但都没有效果。然后Maven来拯救我了。我们可以通过Maven POM文件设置环境变量或系统属性,我认为这是进行基于Maven的项目单元测试的最佳方式。下面是我在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中尝试了这种方法,所以我不清楚Maven 2.x会发生什么。

6
这个解决方案是最好的,应该被采纳,因为你不需要任何额外的东西,比如一个库。Maven本身就足够方便了。谢谢@RLD。 - Semo
4
它需要使用Maven,这比使用一个库要求更高。这将使Junit测试与pom联系在一起,现在测试必须始终通过mvn执行,而不能像通常那样直接在IDE上运行。 - Chirlo
@RLD,我所知道的在Eclipse中唯一的方法是将其作为“Maven”运行配置而不是Junit运行,这更加繁琐,只有文本输出而没有正常的Junit视图。而且我不太明白你所说的整洁简明的代码和必须在多个地方编写代码的观点。对我来说,在pom中拥有测试数据,然后在Junit测试中使用它们比将它们放在一起更加模糊。最近我也遇到了这种情况,最终采用了MathewFarwell的方法,无需库/ pom技巧,一切都在同一个测试中。 - Chirlo
3
这将会使环境变量被硬编码,并且无法在 System.getenv 的一个调用中更改,正确吗? - Ian Stewart
希望这能帮到其他人:如果环境变量不起作用,请检查是否将forkCount设置为0,如果是,则必须将其设置为1,否则surefire无法设置环境! - funfried
显示剩余3条评论

21

我认为最简洁的方法是使用Mockito.spy()。这比创建单独的类进行模拟和传递要更加轻量级。

将您的环境变量获取移到另一个方法中:

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

15

对于JUnit 4用户,建议使用System Lambda,如Stefan Birkner所建议。

如果您正在使用JUnit 5,则有JUnit Pioneer扩展包。它带有@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"));
}

12

我认为这还没有被提到过,但你也可以使用 Powermockito:

给定:

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

14
when(System.getenv("FOO_VAR_1")).thenReturn("test-foo-var-1") 会导致 org.mockito.exceptions.misusing.MissingMethodInvocationException: when()需要一个参数,必须是“对模拟对象的方法调用”。 错误。 - Andremoniy
1
与其使用when(System.getenv("FOO_VAR_1")),可以使用when(System.getenv(eq("FOO_VAR_1")))。eq来自于org.mockito.Mockito.eq。 - Piyush Vishwakarma
6
当别人表现出重视并提供导入信息而不是默认它们太显而易见而省略时,我非常感激。许多时候,一个类存在于多个包中,这使得选择变得不那么明显。 - Woodsman
不适合嘲笑java.lang.System的静态方法,以避免干扰类加载,从而导致无限循环。 - Komoo

10

解耦Java代码与环境变量,提供一个更抽象的变量读取器,通过EnvironmentVariableReader来实现你的代码进行测试读取。

然后在你的测试中,你可以提供一个不同的变量读取器实现,以提供你的测试值。

依赖注入可以帮助解决这个问题。



7
尽管我认为这个答案是适用于Maven项目的最佳方案,但也可以通过反射来实现(在Java 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);
    }
}

1
谢谢!我的Java版本似乎没有theCaseInsensitiveEnvironment,而是有一个字段theEnvironment,如下所示: envMap = new HashMap<>(); Class clazz = Class.forName("java.lang.ProcessEnvironment"); Field theEnvironmentField = clazz.getDeclaredField("theEnvironment"); Field theUnmodifiableEnvironmentField = clazz.getDeclaredField("theUnmodifiableEnvironment"); removeStaticFinalAndSetValue(theEnvironmentField, envMap); removeStaticFinalAndSetValue(theUnmodifiableEnvironmentField, envMap); - Intenex

5

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

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

2
你忘了提到你正在使用JMockit。 :) 无论如何,这个解决方案也可以很好地与JUnit 5一起使用。 - Ryan J. McDonough

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接