ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

由于这是为了测试,系统规则单元测试规则可能是目前最好的答案。
仅适用于在使用 JUnit 5 时对相同问题感兴趣的人:stackoverflow.com/questions/46846503/…
@FelipeMartinsMelo 这个问题是关于系统属性的,而不是环境变量。下面是一个与 JUnit 5 兼容的环境变量解决方案:stackoverflow.com/a/63494695/3429133
这是为 JUNIT 设置环境变量的技巧:stackoverflow.com/questions/318239/…

S
Stefan Birkner

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 自述文件似乎都没有回答我的问题。
M
Matthew Farwell

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

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%,请使用此答案和上一个答案来测试此服务
R
RLD

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

我按照 Stefan Birkner 的建议选择了系统规则。它的使用很简单。但迟早,我发现这种行为不稳定。在一次运行中,它起作用了,在下一次运行中它失败了。我调查并发现系统规则适用于 JUnit 4 或更高版本。但在我的情况下,我使用了一些依赖于 JUnit 3 的罐子。所以我跳过了系统规则。有关它的更多信息,您可以在此处找到 @Rule 注释在 JUnit 中使用 TestSuite 时不起作用。接下来我尝试通过 Java 提供的 Process Builder 类创建环境变量。这里通过Java Code我们可以创建一个环境变量,但是你需要知道我没有知道的进程或程序名称。它还为子进程创建环境变量,而不是为主进程。

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


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

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

b
beatngu13

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

M
Mangusta

我认为尚未提到这一点,但您也可以使用 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
当人们展示导入而不是假设它们太明显而无法添加时,我真的很感激。很多时候,一个类存在于多个包中,因此选择不明显。
A
Andrea Colleoni

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

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

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


C
Community

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


G
George Z.

尽管我认为 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); ```
M
Muthu

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

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
S
Sebastian Luna

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

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

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

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

org.mockito.exceptions.base.MockitoException: It is not possible to mock static methods of java.lang.System to avoid interfering with class loading what leads to infinite loops
A
Ashley Frieze

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 的系统规则几乎解决了这个问题,而系统 Lambda 解决的问题比系统规则少。 System Stubs 更普遍地解决了这个问题。也就是说,JUnit Pioneer 库还有其他功能。
注意:以上适用于 Java 16+
O
Ola Aronsson

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

mvn <cmds> -DmySystemPop=mySystemPropValue

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

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

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


T
Tihamer

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


a
ahmednabil88

简单地

添加下面的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


C
Cygnusx1

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


e
ejaenv

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

D
Dimitri

如果要在 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变量