【问题标题】:How to test code dependent on environment variables using JUnit?如何使用 JUnit 测试依赖于环境变量的代码?
【发布时间】:2011-11-17 14:36:19
【问题描述】:

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

我一般都见过some ways to set environment variables in Java,但我对它的单元测试方面更感兴趣,特别是考虑到测试不应相互干扰。

【问题讨论】:

标签: java unit-testing testing junit environment-variables


【解决方案1】:

System Lambda 有一个方法withEnvironmentVariables 用于设置环境变量。

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

完全披露:我是这两个库的作者。

【讨论】:

  • 我用这个作为@ClassRule,使用后是否需要重置或清除,如果是那么如何?
  • 你不需要。执行完类中的所有测试后,规则会自动重置原始环境变量。
  • import org.junit.contrib.java.lang.system.EnvironmentVariables; 您需要在项目中添加依赖项com.github.stefanbirkner:system-rules。它在 MavenCentral 中可用。
  • 这里是添加依赖的说明:stefanbirkner.github.io/system-rules/download.html
  • Stefan,这听起来棒极了——但这会改变被测代码的环境变量值吗?如果答案很明显,请原谅我,但您的答案和 SystemRules 自述文件似乎都没有回答我的问题。
【解决方案2】:

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

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() 获取环境变量。

【讨论】:

  • 我知道这个问题很老,但我想说这是正确的答案。接受的答案鼓励对 System 隐藏依赖的不良设计,而这个答案鼓励将 System 视为应该注入的另一个依赖项的正确设计。
  • 如果您不关心覆盖率是否为 100%,但更关心良好的设计和关注点分离,请寻求此答案。如果您真的想达到 100%,请使用此答案和上一个答案来测试此服务
【解决方案3】:

在类似的情况下,我必须编写依赖于环境变量测试用例,我尝试了以下操作:

  1. 按照 Stefan Birkner 的建议,我选择了 System Rules。它的使用很简单。但迟早,我发现这种行为不稳定。在一次运行中,它起作用了,在下一次运行中它失败了。 我调查并发现系统规则在 JUnit 4 或更高版本上运行良好。但在我的案例中,我使用了一些依赖于 JUnit 3 的 Jar。所以我跳过了系统规则。更多信息请点击此处@Rule annotation doesn't work while using TestSuite in JUnit
  2. 接下来我尝试通过Java提供的Process Builder类创建环境变量。这里通过 Java 代码我们可以创建一个环境变量,但是你需要知道 processprogram 名称,而我没有。它还为子进程创建环境变量,而不是为主进程。

我使用上述两种方法浪费了一天,但无济于事。然后 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 没有任何了解。

【讨论】:

  • 这个解决方案是最好的,应该是被接受的,因为你不需要任何额外的东西,比如一个库。仅 Maven 就足够方便了。谢谢@RLD
  • @Semo 它需要 maven,这比使用 lib 的要求要大得多。它将 Junit Test 与 pom 耦合在一起,现在测试总是需要从 mvn 执行,而不是像往常一样直接在 IDE 上运行。
  • @Chirlo,这取决于您希望程序与什么相关联。使用 Maven,您可以在一处进行配置并编写简洁明了的代码。如果使用库,则必须在多个地方编写代码。关于运行 JUnit 的观点,即使您使用 Maven,也可以从 Eclipse 等 IDE 运行 JUnit。
  • @RLD,我在 Eclipse 中知道的唯一方法是将其作为“Maven”运行配置而不是 Junit 来运行,这更加麻烦并且只有文本输出而不是正常的 Junit 视图.而且我不太理解您关于简洁明了的代码以及必须在多个地方编写代码的观点。对我来说,将测试数据放在 pom 中然后在 Junit 测试中使用比将它们放在一起更晦涩难懂。我最近遇到了这种情况,最终遵循了 MathewFarwell 的方法,不需要库/pom 技巧,所有东西都在同一个测试中。
  • 这使得环境变量被硬编码,并且它们不能从 System.getenv 的一次调用更改为下一次调用。对吗?
【解决方案4】:

我认为最简洁的方法是使用 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());
}

【讨论】:

    【解决方案5】:

    我认为这还没有被提及,但您也可以使用 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");
        }
    }
    

    【讨论】:

    • when(System.getenv("FOO_VAR_1")).thenReturn("test-foo-var-1") 导致 org.mockito.exceptions.misusing.MissingMethodInvocationException: when() requires an argument which has to be 'a method call on a mock'. 错误
    • 而不是使用 when(System.getenv("FOO_VAR_1")),可以使用 when(System.getenv(eq("FOO_VAR_1"))) eq 来自 org.mockito.Mockito.eq
    • 当人们展示导入而不是假设它们太明显而无法添加时,我真的很感激。很多时候,一个类存在于多个包中,因此选择并不明显。
    【解决方案6】:

    对于 JUnit 4 用户,Stefan Birkner 建议的 System Lambda 非常适合。

    如果您使用的是 JUnit 5,则有 JUnit Pioneer 扩展包。它带有@ClearEnvironmentVariable@SetEnvironmentVariable。来自docs

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

    【讨论】:

      【解决方案7】:

      将 Java 代码与 Environment 变量分离,提供更抽象的变量读取器,您可以使用 EnvironmentVariableReader 来实现您的代码以测试读取。

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

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

      【讨论】:

        【解决方案8】:

        This answer 对问题How do I set environment variables from Java? 提供了一种更改 System.getenv() 中的(不可修改的)映射的方法。因此,虽然它并没有真正改变操作系统环境变量的值,但它可以用于单元测试,因为它确实改变了System.getenv 将返回的内容。

        【讨论】:

          【解决方案9】:

          尽管我认为this answer 最适合 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);
              }
          }
          

          【讨论】:

          • 谢谢!我的 Java 版本似乎没有theCaseInsensitiveEnvironment,而是有一个字段theEnvironment,如下所示:``` envMap = new HashMap();类> clazz = Class.forName("java.lang.ProcessEnvironment"); Field theEnvironmentField = clazz.getDeclaredField("theEnvironment"); Field theUnmodifiableEnvironmentField = clazz.getDeclaredField("theUnmodifiableEnvironment"); removeStaticFinalAndSetValue(theEnvironmentField, envMap); removeStaticFinalAndSetValue(theUnmodifiableEnvironmentField, envMap); ```
          【解决方案10】:

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

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

          【讨论】:

          • 您忘记提及您正在使用 JMockit。 :) 无论如何,这个解决方案也适用于 JUnit 5
          【解决方案11】:

          您可以使用Powermock 来模拟呼叫。喜欢:

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

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

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

          【讨论】:

            【解决方案12】:

            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 之类的东西很好地互操作,这可能会设置子测试所需的一些资源。

            【讨论】:

            • Pioneer 的环境变量扩展实现了BeforeAllCallbackBeforeEachCallback,但值目前仅限于常量表达式。
            • 每个工具都有自己的优势/劣势。 System Stubs 解决了我反复遇到但在其他地方没有问题的问题。 Stefan Birkner 的系统规则几乎解决了这个问题,而 System Lambda 解决的问题比系统规则少。 System Stubs 更普遍地解决了这个问题。也就是说,JUnit Pioneer 库还有其他功能。
            【解决方案13】:

            在上面的建议中,很多重点是发明方法在运行时来传递变量,设置它们并清除它们等等..?但是为了“结构化”地测试事物,我猜你想为不同的场景提供不同的测试套件?很像你想运行“更重”的集成测试构建,而在大多数情况下,你只想跳过它们。但是,您不会尝试“发明在运行时设置内容的方法”,而只是告诉 maven 你想要什么?过去,告诉 maven 通过配置文件运行特定测试需要做很多工作,如果你在周围搜索别人会建议通过 springboot 进行测试(但如果你没有将 springboot monstrum 拖到你的项目中,这似乎很可怕'只是运行 JUnits' 的足迹,对吧?)。否则这将意味着或多或少不方便的 POM XML 杂耍负载,这也很烦人,让我们说,“九十年代的举动”,就像仍然坚持用“用 XML 制作春豆”一样不方便,炫耀你的 ultimate 600 行 logback.xml 之类的......?

            现在,你可以只使用 Junit 5(这个例子是 maven 的,更多细节可以在这里找到JUnit 5 User Guide 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-testing 时,您的 @IntegrationTest 注释测试类/方法才会触发。或者,如果您不想使用(和设置)特定的 Maven 配置文件,而只是通过 'trigger' 系统属性传递

            mvn <cmds> -DmySystemPop=mySystemPropValue
            

            并调整您的注释界面以触发它(是的,还有一个@EnabledIfSystemProperty)。或者确保你的 shell 设置为包含“你需要的任何东西”,或者,正如上面所建议的,实际上通过你的 POM XML 添加系统环境是“痛苦的”。

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

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

            【讨论】:

              【解决方案14】:

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

              【讨论】:

                【解决方案15】:

                简单

                添加下面的maven依赖

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

                更多详情请参考
                https://www.baeldung.com/java-system-stubs

                【讨论】:

                  【解决方案16】:

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

                  【讨论】:

                    【解决方案17】:

                    如果你想在Java中检索环境变量的信息,你可以调用方法:System.getenv();。作为属性,此方法返回一个 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); 
                         }   
                     }
                    

                    【讨论】:

                    • 重点不是不要从junit测试中访问env变量
                    【解决方案18】:

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

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 2016-06-21
                      • 1970-01-01
                      • 1970-01-01
                      • 2021-06-12
                      • 1970-01-01
                      • 2019-03-11
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多