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


139

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

我已经大致了解了一些在Java设置环境变量的方法,但是我对它的单元测试方面更感兴趣,尤其是考虑到测试之间不应相互干扰。


由于这是为了测试,因此“系统规则”单元测试规则可能是目前的最佳答案。
Atifm

3
只是对于那些有兴趣在同样的问题,而使用JUnit 5:stackoverflow.com/questions/46846503/...
菲利佩马丁斯梅洛

Answers:


199

系统规则提供了用于设置环境变量的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

不用了 执行类中的所有测试后,规则将自动重置原始环境变量。
Stefan Birkner '16

此方法仅适用于JUnit 4或更高版本。不建议将其用于JUnit 3或更低版本,或者将JUnit 4和JUnit 3混合使用
。– RLD

2
import org.junit.contrib.java.lang.system.EnvironmentVariables;您将需要com.github.stefanbirkner:system-rules在项目中添加依赖项。它在MavenCentral中可用。
Jean Bob


77

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

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


1
我知道这个问题很旧,但是我想说这是正确的答案。可接受的答案鼓励不良设计,并隐含对System的依赖,而此答案鼓励进行适当的设计,将System视为应该注入的另一个依赖。
安德鲁

30

在类似的情况下,我不得不编写依赖于Environment Variable的Test Case,我尝试了以下操作:

  1. 我按照Stefan Birkner的建议去了系统规则。它的使用很简单。但是,我发现这种行为迟早会发生变化。在一次运行中,它起作用,而在下一次运行中,它失败。我调查发现,系统规则可与JUnit 4或更高版本一起使用。但是在我的情况下,我使用了一些依赖于JUnit 3的 Jars 。所以我跳过了系统规则。更多关于它,你可以在这里找到,而在使用JUnit的TestSuite的@rule标注不起作用
  2. 接下来,我试图创建环境变量,通过进程生成提供一流的Java。在这里,通过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不了解


2
这种解决方案是最好的,应该被接受,因为您不需要像lib这样的任何其他东西。仅Maven就足够方便了。谢谢@RLD
色嫫

@Semo它需要Maven,但比使用lib更大。它将Junit测试耦合到pom,现在始终需要从mvn执行该测试,而不是像通常那样直接在IDE上运行它。
Chirlo

@Chirlo,这取决于您希望程序配合什么。使用Maven,您可以在一个地方进行配置,并编写简洁明了的代码。如果使用库,则必须在多个位置编写代码。关于运行JUnits的观点,即使您使用Maven,也可以从IDE(如Eclipse)运行JUnits。
RLD

@RLD,我在Eclipse中知道的唯一方法是将其作为“ Maven”运行配置而不是Junit来运行,后者更加麻烦,并且仅具有文本输出而不是普通的Junit视图。而且我不太喜欢您的简洁简洁的代码,而不必在多个地方编写代码。对我而言,将测试数据存储在pom中,然后再用于Junit测试中,比将它们放在一起要模糊得多。我最近处于这种情况,最终遵循MathewFarwell的方法,不需要库/ pom技巧,并且所有内容都在同一测试中。
Chirlo

1
这使得环境变量被硬编码,并且不能将它们从System.getenv的一次调用更改为下一次。正确?
伊恩·斯图尔特

12

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

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

8
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'.错误
Andremoniy

10

将Java代码与Environment变量分离,从而提供了一个更抽象的变量读取器,您可以使用EnvironmentVariableReader实现该代码读取器以测试读取的内容。

然后,在测试中,您可以提供变量读取器的另一种实现,该实现提供您的测试值。

依赖注入可以帮助实现这一点。



4

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

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

1
您忘了提到您正在使用JMockit。:)无论如何,此解决方案也适用于JUnit 5
Ryan J. McDonough

1

即使我认为这个答案对于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 <>(); Class <?> clazz = Class.forName(“ java.lang.ProcessEnvironment”); Field theEnvironmentField = clazz.getDeclaredField(“ theEnvironment”); 字段the UnmodifiableEnvironmentField = clazz.getDeclaredField(“ theUnmodifiableEnvironment”); removeStaticFinalAndSetValue(theEnvironmentField,envMap); removeStaticFinalAndSetValue(theUnmodifiableEnvironmentField,envMap); ```
Intenex

-2

好了,您可以使用setup()方法来声明环境的不同值。常量中的变量。然后在用于测试不同方案的测试方法中使用这些常量。


-2

如果要检索有关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); 
     }   
 }

1
主要的一点是不可不从JUnit测试访问ENV变量
Tanmoy尔吉

-2

我使用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"));
         }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.