zjffdu
zjffdu

Reputation: 28774

How to unset environment variable before testing in maven

I have some unit test which depends on environment variable.

I'd like to unset these environment variable before testing using maven.

Question: How can I achieve that?

Upvotes: 5

Views: 6604

Answers (1)

A_Di-Matteo
A_Di-Matteo

Reputation: 27812

Unfortunately, in this case you cannot use the environmentVariables option of the Maven Surefire Plugin, mainly because it would only work to add new environment variables but not override (or reset, which is actually equals to override to empty value) an existing variable.

Also note: an ant run wrapped in Maven and executed before the test would not work either.

The proposed solution is based on Java, especially on this approach proposed in another SO post. Although not advisable for application code, this hack may be acceptable for test code.

You could add the following class to your test code (under src/test/java):

package com.sample;

import java.lang.reflect.Field;
import java.util.Map;

public class EnvHack {

    @SuppressWarnings("unchecked")
    public static void resetEnvironmentVariable(String name, String value) throws Exception {
        Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment");
        Field theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment");
        theEnvironmentField.setAccessible(true);
        Map<String, String> env = (Map<String, String>) theEnvironmentField.get(null);
        env.put(name, value);
        Field theCaseInsensitiveEnvironmentField = processEnvironmentClass
                .getDeclaredField("theCaseInsensitiveEnvironment");
        theCaseInsensitiveEnvironmentField.setAccessible(true);
        Map<String, String> cienv = (Map<String, String>) theCaseInsensitiveEnvironmentField.get(null);
        cienv.put(name, value);
    }

    public static void main(String[] args) throws Exception {
        resetEnvironmentVariable("test_prop", "test_value");
    }
}

The class above is basically hacking Java API to change in memory the values of the environment variables. As such, they can be set to different values, reset and even unset actually (remove it from the map).

Since the class is now part of your test code, you have several options:

  • Use the class in the @Before methods (or @BeforeClass, with a certain difference) of a certain JUnit test case (before every JUnit method of the concerned class)
  • Use it within a JUnit test method (custom and narrowed usage)
  • Run its main method before any executed JUnit test (in a more global way) as explained below (and probably answering the question, even though other scenarios are also worth to mention, imho).

Let's have a look at each possible solution.

Use the class in the @Before methods of a certain JUnit test case

package com.sample;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class MainTest {

    @Before
    public void init() throws Exception {
        EnvHack.resetEnvironmentVariable("test_prop", "test_value");
    }

    @Test
    public void testEnvProperty() throws Exception {
        String s = System.getenv("test_prop");
        Assert.assertEquals(s, "test_value");
    }

}

This solution can be used per test class and when a set of tests (methods) share the same requirements (suggestion: if they don't, it may be an hint, probably some method should be moved out).

Use it within a JUnit test method

package com.sample;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class MainTest {

    @Before
    public void init() throws Exception {
        EnvHack.resetEnvironmentVariable("test_prop", "test_value");
    }

    @Test
    public void testEnvProperty() throws Exception {
        EnvHack.resetEnvironmentVariable("test_prop", "test_value2");
        String s = System.getenv("test_prop");
        Assert.assertEquals(s, "test_value2");
    }

}

This solution gives the highest freedom: you can play with exactly the concerned variable exactly where required, although may suffer of code duplication it could also enforce tests isolation.

Run its main method before any executed JUnit test

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>1.4.0</version>
    <executions>
        <execution>
            <phase>process-test-classes</phase>
            <goals>
                <goal>java</goal>
            </goals>
            <configuration>
                <mainClass>com.sample.EnvHack</mainClass>
                <classpathScope>test</classpathScope>
            </configuration>
        </execution>
    </executions>
</plugin>

Note what we are doing in this case:

  • We are invoking the java goal of the Exec Maven Plugin to actually invoke the mainClass of our env hack class.
  • We are invoking it with classPathScope set to test in order to make it visible to the Enforcer Plugin
  • We are running it as part of the process-test-classes phase just to make sure it is executed before the test phase and hence before any test.

This solution centralizes the whole prepare environment procedure, once for all tests.


On the other side, you may also consider to use mocking in your tests. This is not a centralized option (unless you code it) but could give further hints and hence worth to mention. Here is a sample code resetting an environment variable via PowerMock

package com.sample;

import org.junit.*;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest(System.class)
public class EnvTest {

    @Before
    public void init() {
        PowerMockito.mockStatic(System.class);
        Mockito.when(System.getenv("TEST_PROP")).thenReturn("TEST_VALUE");
    }

    @Test
    public void test() {
        String var = System.getenv("TEST_PROP");
        Assert.assertEquals("TEST_VALUE", var);
    }
}

This is similar to the first and second approach proposed above.

Note that to make it work you need to add the following dependencies to your POM:

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-api-mockito</artifactId>
        <version>1.5</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-core</artifactId>
        <version>1.5</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-module-junit4</artifactId>
        <version>1.5</version>
        <scope>test</scope>
    </dependency>
</dependencies>

General note: indeed having tests which are not fully isolated and depends on the existence (or the absence) of an environment variable is not advisable. You may run into maintenance issues or have more nasty troubleshooting, for colleagues or for your future yourself. So, if you really need it, better to properly document it.

Upvotes: 2

Related Questions