我有一段使用环境变量的 Java 代码,代码的行为取决于这个变量的值。我想用不同的环境变量值测试这段代码。我怎样才能在 JUnit 中做到这一点?
我一般都看过 some ways to set environment variables in Java,但我对它的单元测试方面更感兴趣,特别是考虑到测试不应相互干扰。
库 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"));
}
}
完全披露:我是这两个库的作者。
通常的解决方案是创建一个类来管理对该环境变量的访问,然后您可以在测试类中模拟它。
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() 获取环境变量。
在类似的情况下,我必须编写依赖于环境变量的测试用例,我尝试了以下操作:
我按照 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 没有任何概念。
我认为最干净的方法是使用 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());
}
对于 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"));
}
我认为尚未提到这一点,但您也可以使用 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'.
错误
将 Java 代码与 Environment 变量解耦,提供一个更抽象的变量阅读器,您可以使用 EnvironmentVariableReader 来实现您的代码以测试读取。
然后在您的测试中,您可以提供变量阅读器的不同实现,以提供您的测试值。
依赖注入可以帮助解决这个问题。
问题 How do I set environment variables from Java? 的 This answer 提供了一种更改 System.getenv() 中的(不可修改的)映射的方法。因此,虽然它并没有真正改变操作系统环境变量的值,但它可以用于单元测试,因为它确实改变了 System.getenv 将返回的内容。
尽管我认为 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);
}
}
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); ```
希望问题得到解决。我只是想告诉我的解决方案。
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);
}
};
您可以使用 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
库 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
之类的东西可以很好地互操作,这可能会设置子测试所需的一些资源。
BeforeAllCallback
和 BeforeEachCallback
,但值目前仅限于常量表达式。
上面的建议中有很多重点是在运行时发明方法来传递变量,设置它们并清除它们等等..?但是为了“结构化”地测试事物,我猜你想为不同的场景提供不同的测试套件?很像你想运行“更重”的集成测试构建,而在大多数情况下,你只想跳过它们。但是你不会尝试“发明在运行时设置东西的方法”,而是告诉 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')。
一种缓慢、可靠、老派的方法总是适用于每种语言(甚至语言之间)的每个操作系统,是将您需要的“系统/环境”数据写入一个临时文本文件,在需要时读取它,然后擦除它。当然,如果您是并行运行的,那么您需要文件的唯一名称,并且如果您将敏感信息放入其中,那么您需要对其进行加密。
简单地
添加下面的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
那么你可以使用 setup() 方法来声明你的环境的不同值。常量中的变量。然后在用于测试不同场景的测试方法中使用这些常量。
我使用 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"));
}
}
如果要在 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);
}
}
不定期副业成功案例分享
import org.junit.contrib.java.lang.system.EnvironmentVariables;
您需要在项目中添加依赖项com.github.stefanbirkner:system-rules
。它在 MavenCentral 中可用。