ChatGPT解决这个技术问题 Extra ChatGPT

How to test code dependent on environment variables using JUnit?

I have a piece of Java code which uses an environment variable and the behaviour of the code depends on the value of this variable. I would like to test this code with different values of the environment variable. How can I do this in JUnit?

I've seen some ways to set environment variables in Java in general, but I'm more interested in unit testing aspect of it, especially considering that tests shouldn't interfere with each other.

Since this is for testing, the System Rules unit test rule may be the best answer at the present.
Just for those interested in the same question while using JUnit 5: stackoverflow.com/questions/46846503/…
@FelipeMartinsMelo that question is about system properties, not environment variables. Here is a JUnit 5-compatible solution for environment variables: stackoverflow.com/a/63494695/3429133
here is a hack to set env variables for JUNIT: stackoverflow.com/questions/318239/…

S
Stefan Birkner

The library System Lambda has a method withEnvironmentVariables for setting environment variables.

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

For Java 5 to 7 the library System Rules has a JUnit rule called EnvironmentVariables.

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

Full disclosure: I'm the author of both libraries.


I am using this as @ClassRule, do I need to reset or clear it after use, if yes then how?
You don't need to. The original environment variables are automatically reset by the rule after all tests in the class are executed.
import org.junit.contrib.java.lang.system.EnvironmentVariables; You will need to add the dependency com.github.stefanbirkner:system-rules in your project. It's available in MavenCentral.
Here are the instructions to add the dependency: stefanbirkner.github.io/system-rules/download.html
Stefan, this sounds fantastic - but does this change the value of the environment variables for the code under test? Forgive me if the answer is obvious, but neither your answer nor the SystemRules readme seem to answer my question.
M
Matthew Farwell

The usual solution is to create a class which manages the access to this environmental variable, which you can then mock in your test class.

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

The class under test then gets the environment variable using the Environment class, not directly from System.getenv().


I know this question is old, but I wanted to say that this is the correct answer. The accepted answer encourages poor design with a hidden dependency on System, whereas this answer encourages a proper design treating System as just another dependency that should be injected.
seek this answer if you don't care about the coverage percentage being 100% but care more about good design and separation of concerns. If you really want to reach the 100% use this and the previous answer to test this service
R
RLD

In a similar situation like this where I had to write Test Case which is dependent on Environment Variable, I tried following:

I went for System Rules as suggested by Stefan Birkner. Its use was simple. But sooner than later, I found the behavior erratic. In one run, it works, in the very next run it fails. I investigated and found that System Rules work well with JUnit 4 or higher version. But in my cases, I was using some Jars which were dependent on JUnit 3. So I skipped System Rules. More on it you can find here @Rule annotation doesn't work while using TestSuite in JUnit. Next I tried to create Environment Variable through Process Builder class provided by Java. Here through Java Code we can create an environment variable, but you need to know the process or program name which I did not. Also it creates environment variable for child process, not for the main process.

I wasted a day using the above two approaches, but of no avail. Then Maven came to my rescue. We can set Environment Variables or System Properties through Maven POM file which I think best way to do Unit Testing for Maven based project. Below is the entry I made in POM file.

    <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>

After this change, I ran Test Cases again and suddenly all worked as expected. For reader's information, I explored this approach in Maven 3.x, so I have no idea on Maven 2.x.


This solution is the best and should be the accepted one, because you won't need anything additional like a lib. Maven alone is handy enough. Thank you @RLD
@Semo it requires maven though, which is much bigger requirement than using a lib. It couples the Junit Test to the pom, and the test now always need to be executed from mvn, instead of running it directly on the IDE the usual way.
@Chirlo, it depends on what you want your program to tie with. Using Maven, you can configure in one place and write neat and concise code. If you use library, you have to write code in multiple places. Regarding your point of running JUnits, you can run JUnits from IDE like Eclipse even if you use Maven.
@RLD, the only way I know of in Eclipse would be running it as a 'Maven' run configuration instead of a Junit which is much more cumbersome and just has text output instead of the normal Junit view. And I don't quite follow your point of neat and concise code and having to write code in multiple places. For me, having test data in the pom that is then used in a Junit test is more obscure than having them together. I was in this situation recently and ended up following MathewFarwell's approach, no need for libraries/pom tricks and everything is together in the same test.
This makes the environment variables hard-coded, and they can't be changed from one invocation of System.getenv to the next. Correct?
p
pgkelley

I think the cleanest way to do this is with Mockito.spy(). It's a bit more lightweight than creating a separate class to mock and pass around.

Move your environment variable fetching to another method:

@VisibleForTesting
String getEnvironmentVariable(String envVar) {
    return System.getenv(envVar);
}

Now in your unit test do this:

@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

For JUnit 4 users, System Lambda as suggested by Stefan Birkner is a great fit.

In case you are using JUnit 5, there is the JUnit Pioneer extension pack. It comes with @ClearEnvironmentVariable and @SetEnvironmentVariable. From the docs:

The @ClearEnvironmentVariable and @SetEnvironmentVariable annotations can be used to clear, respectively, set the values of environment variables for a test execution. Both annotations work on the test method and class level, are repeatable as well as combinable. After the annotated method has been executed, the variables mentioned in the annotation will be restored to their original value or will be cleared if they didn't have one before. Other environment variables that are changed during the test, are not restored.

Example:

@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

I don't think this has been mentioned yet, but you could also use Powermockito:

Given:

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

You could do the following:

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") causes org.mockito.exceptions.misusing.MissingMethodInvocationException: when() requires an argument which has to be 'a method call on a mock'. error
Instead of using when(System.getenv("FOO_VAR_1")), one could use when(System.getenv(eq("FOO_VAR_1"))) eq comes from org.mockito.Mockito.eq
I really appreciate it when people show imports instead of assuming they're too obvious to add. Many times a class is found in multiple packages making the choice non-obvious.
A
Andrea Colleoni

Decouple the Java code from the Environment variable providing a more abstract variable reader that you realize with an EnvironmentVariableReader your code to test reads from.

Then in your test you can give an different implementation of the variable reader that provides your test values.

Dependency injection can help in this.


C
Community

This answer to the question How do I set environment variables from Java? provides a way to alter the (unmodifiable) Map in System.getenv(). So while it doesn't REALLY change the value of the OS environment variable, it can be used for unit testing as it does change what System.getenv will return.


G
George Z.

Even though I think this answer is the best for Maven projects, It can be achieved via reflect as well (tested in 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);
    }
}

Thanks for this! My version of Java doesn't seem to have theCaseInsensitiveEnvironment and instead has a field theEnvironment, like the following: ``` 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); ```
M
Muthu

Hope the issue is resolved. I just thought to tell my solution.

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

You forgot to mention that you're using JMockit. :) Regardless, this solution also works great with JUnit 5
S
Sebastian Luna

You can use Powermock for mocking the call. Like:

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

You can also mock all the calls with:

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

The library https://github.com/webcompere/system-stubs/tree/master/system-stubs-jupiter - a fork of system-lambda - provides a JUnit 5 plug-in:

@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
    }

}

The https://junit-pioneer.org/ alternative requires environment variable values to be known at compile time. The above also supports the setting of environment variables in the @BeforeAll, which means it interoperates well with things like Testcontainers that might set up some resources needed by child tests.


Pioneer's environment variable extension implements both BeforeAllCallback and BeforeEachCallback, but values are currently limited to constant expressions.
Each tool has its own strengths/weaknesses. System Stubs solves a problem I was repeatedly having and didn't have a problem elsewhere. Stefan Birkner's System Rules nearly solved that problem, and System Lambda solved it less than System Rules. System Stubs solves it more universally. That said, the JUnit Pioneer library has other features too.
Note: the above works with Java 16+
O
Ola Aronsson

A lot of focus in the suggestions above on inventing ways in runtime to pass in variables, set them and clear them and so on..? But to test things 'structurally', I guess you want to have different test suites for different scenarios? Pretty much like when you want to run your 'heavier' integration test builds, whereas in most cases you just want to skip them. But then you don't try and 'invent ways to set stuff in runtime', rather you just tell maven what you want? It used to be a lot of work telling maven to run specific tests via profiles and such, if you google around people would suggest doing it via springboot (but if you haven't dragged in the springboot monstrum into your project, it seems a horrendous footprint for 'just running JUnits', right?). Or else it would imply loads of more or less inconvenient POM XML juggling which is also tiresome and, let's just say it, 'a nineties move', as inconvenient as still insisting on making 'spring beans out of XML', showing off your ultimate 600 line logback.xml or whatnot...?

Nowadays, you can just use Junit 5 (this example is for maven, more details can be found here 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>

and then

    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>

and then in your favourite utility lib create a simple nifty annotation class such as

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@EnabledIfEnvironmentVariable(named = "MAVEN_CMD_LINE_ARGS", matches = "(.*)integration-testing(.*)")
public @interface IntegrationTest {}

so then whenever your cmdline options contain -Pintegration-testing for instance, then and only then will your @IntegrationTest annotated test-class/method fire. Or, if you don't want to use (and setup) a specific maven profile but rather just pass in 'trigger' system properties by means of

mvn <cmds> -DmySystemPop=mySystemPropValue

and adjust your annotation interface to trigger on that (yes, there is also a @EnabledIfSystemProperty). Or making sure your shell is set up to contain 'whatever you need' or, as is suggested above, actually going through 'the pain' adding system env via your POM XML.

Having your code internally in runtime fiddle with env or mocking env, setting it up and then possibly 'clearing' runtime env to change itself during execution just seems like a bad, perhaps even dangerous, approach - it's easy to imagine someone will always sooner or later make a 'hidden' internal mistake that will go unnoticed for a while, just to arise suddenly and bite you hard in production later..? You usually prefer an approach entailing that 'given input' gives 'expected output', something that is easy to grasp and maintain over time, your fellow coders will just see it 'immediately'.

Well long 'answer' or maybe rather just an opinion on why you'd prefer this approach (yes, at first I just read the heading for this question and went ahead to answer that, ie 'How to test code dependent on environment variables using JUnit').


T
Tihamer

One slow, dependable, old-school method that always works in every operating system with every language (and even between languages) is to write the "system/environment" data you need to a temporary text file, read it when you need it, and then erase it. Of course, if you're running in parallel, then you need unique names for the file, and if you're putting sensitive information in it, then you need to encrypt it.


a
ahmednabil88

Simply

Add below maven dependency

<!-- 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>

Inside your test, you can use something similar:

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

Reference for more details
https://www.baeldung.com/java-system-stubs


C
Cygnusx1

Well you can use the setup() method to declare the different values of your env. variables in constants. Then use these constants in the tests methods used to test the different scenario.


e
ejaenv

I use System.getEnv() to get the map and I keep as a field, so I can mock it:

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

If you want to retrieve informations about the environment variable in Java, you can call the method : System.getenv();. As the properties, this method returns a Map containing the variable names as keys and the variable values as the map values. Here is an example :

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

The method getEnv() can also takes an argument. For instance :

String myvalue = System.getEnv("MY_VARIABLE");

For testing, I would do something like this :

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

The main point is not not to access the env variables from the junit test