130

I'd like to write some tests that check the XML Spring configuration of a deployed WAR. Unfortunately some beans require that some environment variables or system properties are set. How can I set an environment variable before the spring beans are initialized when using the convenient test style with @ContextConfiguration?

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:whereever/context.xml")
public class TestWarSpringContext { ... }

If I configure the application context with annotations, I don't see a hook where I can do something before the spring context is initialized.

Dr. Hans-Peter Störr
  • 25,298
  • 30
  • 102
  • 139

11 Answers11

165

You can initialize the System property in a static initializer:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:whereever/context.xml")
public class TestWarSpringContext {

    static {
        System.setProperty("myproperty", "foo");
    }

}

The static initializer code will be executed before the spring application context is initialized.

Jimmy Praet
  • 2,230
  • 1
  • 15
  • 14
  • 26
    Silly me - OK, that would work. Even better: probably a `@BeforeClass` method to set the system property and an `@AfterClass` method to remove it would also work, and nicely clean up after itself. (Didn't try it out, though.) – Dr. Hans-Peter Störr Aug 27 '12 at 15:39
  • 2
    Tried the @BeforeClass - and it worked fine for setting system properties before other properties were set in the test instance – wbdarby Apr 02 '14 at 14:08
  • 1
    Thanks for this. The static thing didnt work but a small method with @BeforeClass worked ! – Midhun Agnihotram Dec 30 '16 at 04:31
  • This mechanism does not work if changing Log4j2 configuration file property. Seems that Spring anyway is being loaded (and so logging incorrectly) before that static piece of code. – lucasvc Mar 13 '19 at 11:35
  • 2
    This way, the system property gets initialized once before spring application context is initialized. How would an approach look like where tests require the system property value to change? For instance, test A requires the value for `myproperty` to be `foo`, but test B requires the the value for `myproperty` to be `somethingelse`. – Martin H. Oct 19 '21 at 13:39
125

Spring @TestPropertySource

The right way to do this, starting with Spring 4.1, is to use a @TestPropertySource annotation.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:whereever/context.xml")
@TestPropertySource(properties = {"myproperty = foo"})
public class TestWarSpringContext {
    ...    
}

See @TestPropertySource in the Spring docs and Javadocs.

Dynamic Properties

If the property is dynamic e.g. a hostname or port from a dynamically created testcontainer, the @DynamicPropertySource annotation can be used. Example with a kafka TestContainer that exposes the container's bootstrap servers:

@DynamicPropertySource
static void kafkaProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
}

See @DynamicPropertySource in the Spring docs and Javadocs.

A Note on Spring Environment Properties vs Environment Variables and System Properties

One popular comment below says:

This provides an Environment property, which is different to an "Environment variable". –

Yes, that's true, BUT if you are using Spring, you shouldn't be directly accessing environment variables or system properties in your code. Spring provides an abstraction over property sources such as configuration files, system properties, and environment variables called the Spring PropertySource. Since code in a Spring environment should be using this abstraction, this is the proper way to inject these properties, bypassing the direct access of environment variables entirely.

If you have a 3rd party library or something that is using environment variables, and you can't abstract or mock that code for your test, then you have a completely different problem, requiring a different non-Spring-specific solution.

Raman
  • 17,606
  • 5
  • 95
  • 112
  • 3
    This annotation also supports a properties file path. – MigDus May 05 '16 at 14:44
  • 2
    I could switch the Spring Cloud Config Client label during tests using `@TestPropertySource(properties={"spring.cloud.config.label=feature/branch"})` – Marcello DeSales Sep 19 '16 at 02:32
  • 13
    Good answer, but sadly didn't work for me, using Spring 4.2.9, the property was always empty. Only the static block worked... Worked for application properties, but not for system properties. – Gregor Feb 06 '18 at 14:56
  • First I saw and tried the static version (which worked), but this Annotation is even cleaner und much more preferable (for me, as it also works like a charm). – BAERUS May 08 '18 at 07:17
  • 10
    This provides an `Environment` property, which is different to an "Environment variable". – OrangeDog Aug 02 '19 at 13:15
  • will this remove the property (myproperty) after the test is executed? – Sajith Oct 03 '19 at 05:25
  • This could work but to maintain 2 properties files and/or to pass all the properties in the string `properties = {"myproperty = foo"}` will be a pain – JRichardsz Jan 27 '23 at 22:31
22

One can also use a test ApplicationContextInitializer to initialize a system property:

public class TestApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>
{
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext)
    {
        System.setProperty("myproperty", "value");
    }
}

and then configure it on the test class in addition to the Spring context config file locations:

@ContextConfiguration(initializers = TestApplicationContextInitializer.class, locations = "classpath:whereever/context.xml", ...)
@RunWith(SpringJUnit4ClassRunner.class)
public class SomeTest
{
...
}

This way code duplication can be avoided if a certain system property should be set for all the unit tests.

anre
  • 3,617
  • 26
  • 33
  • This also works perfectly with Spring Boot 2.x and Junit 5.x (using `@SpringBootTest` or any of the [test slicing](https://spring.io/blog/2016/08/30/custom-test-slice-with-spring-boot-1-4) annotations) – Wim Deblauwe Jan 10 '20 at 16:54
19

All of the answers here currently only talk about the system properties which are different from the environment variables that are more complex to set, esp. for tests. Thankfully, below class can be used for that and the class docs has good examples

EnvironmentVariables.html

A quick example from the docs, modified to work with @SpringBootTest

@SpringBootTest
public class EnvironmentVariablesTest {
   @ClassRule
   public final EnvironmentVariablesRule environmentVariablesRule = new EnvironmentVariablesRule().set("name", "value");

   @Test
   public void test() {
     assertEquals("value", System.getenv("name"));
   }
 }
Uday Singh
  • 155
  • 1
  • 8
Harish Gokavarapu
  • 1,785
  • 17
  • 13
  • 6
    The EnvironmentVariables rules is part of a third party library, uses hacky reflection to change the cached values of the environment in JVM memory and does not even the the actual environment variables. So, I would not like to use it or recommend anyone to do so. – Christian Jun 29 '20 at 10:40
  • It also seems to have a ProvideSystemProperty rule and, weirdly, a RestoreSystemProperties rule. So that could work for system properties, too. – Dr. Hans-Peter Störr May 07 '21 at 06:33
  • With Spring, your code under test shouldn't be accessing environment variables directly. You should be using Spring's property abstraction. See https://stackoverflow.com/a/35763386/430128. – Raman Jul 26 '23 at 15:13
8

If you want your variables to be valid for all tests, you can have an application.properties file in your test resources directory (by default: src/test/resources) which will look something like this:

MYPROPERTY=foo

This will then be loaded and used unless you have definitions via @TestPropertySource or a similar method - the exact order in which properties are loaded can be found in the Spring documentation chapter 24. Externalized Configuration.

blalasaadri
  • 5,990
  • 5
  • 38
  • 58
8

For springboot, here would be the simplest way to do it in my opinion use the @SpringBootTest annotation you can in java:

@SpringBootTest(
    properties = { "spring.application.name=example", "ENV_VARIABLE=secret" }
)
public class ApplicationTest {

    // Write your tests here

}

Or in kotlin you can do:

@SpringBootTest(
    properties = ["spring.application.name=example", "ENV_VARIABLE=secret"]
)
internal class ApplicationKTest {

    // Write your tests here

}

And that's it your test should run overriding the properties with the one you have define in the annotation. Let's say you had an application.yml looking like that:

spring:
  application:
    name: "app"

db:
  username: "user"
  password: ${ENV_VARIABLE:default}

Then during the test it would be:

  • The spring property spring.application.name will return the value "example"
  • The environment variable ENV_VARIABLE will return "secret", so if you use the value db.password in your code it would return "secret".
Sylhare
  • 5,907
  • 8
  • 64
  • 80
3

You can set the System properties as VM arguments.

If your project is a maven project then you can execute following command while running the test class:

mvn test -Dapp.url="https://stackoverflow.com"

Test class:

public class AppTest  {
@Test
public void testUrl() {
    System.out.println(System.getProperty("app.url"));
    }
}

If you want to run individual test class or method in eclipse then :

1) Go to Run -> Run Configuration

2) On left side select your Test class under the Junit section.

3) do the following :

enter image description here

Michael Lihs
  • 7,460
  • 17
  • 52
  • 85
Joby Wilson Mathews
  • 10,528
  • 6
  • 54
  • 53
3

For Unit Tests, the System variable is not instantiated yet when I do "mvn clean install" because there is no server running the application. So in order to set the System properties, I need to do it in pom.xml. Like so:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.21.0</version>
    <configuration>
        <systemPropertyVariables>
            <propertyName>propertyValue</propertyName>
            <MY_ENV_VAR>newValue</MY_ENV_VAR>
            <ENV_TARGET>olqa</ENV_TARGET>
            <buildDirectory>${project.build.directory}</buildDirectory>
        </systemPropertyVariables>
    </configuration>
</plugin>
Gene
  • 10,819
  • 1
  • 66
  • 58
2

If you have a lot of test classes (IT tests that startup tomcat/server), and the tests are failing, you need to set the system property using System.setProperty("ccm.configs.dir", configPath); Since you need to make sure that is set before spring starts, you need to put it in a static context in a class. And to make sure any test that may depend on it gets this set system property, define a simple config class in your test folder setting up that variable. P.S in my case the env variable that was needed was "ccm.configs.dir" Here is what i added in my test folder,

@Configuration
public class ConfigLoader {
  static {
    
    System.setProperty("ccm.configs.dir", "path/to/the/resource");
  }

}

And all my integration test classes were able to get that variable already set by the time they are run.

mykey
  • 1,943
  • 19
  • 13
1

All the answers are grouped in two:

  • pass the env variables one by one using some annotation or System.setProperty;
  • to have another application.properties *yml

Both approaches work but based in several projects, to maintain 2 properties files and/or to pass all the properties in the string properties = {"myproperty = foo"} on each test will be a complicated

This worked for me:

  • Keep just one application.properties
  • Use env variables in the application.properties
truestore.custom.location = ${JRE_CACERT_LOCATION}
truestore.custom.password = ${JRE_CACERT_PASSWORD}
  • In the test that requires the application.properties like @WebMvcTest(FooController.class) and at the same time the env vars declares on application.properties add this code:
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import com.jayway.jsonpath.JsonPath;

@WebMvcTest(HealthController.class)
public class HealthControllerTest {

  static {
    File resourcesDirectory = new File("src/test/resources");
    try (Stream<String> stream = Files.lines(
        Paths.get(resourcesDirectory.getAbsolutePath() + File.separator + "application.env"))) {
      stream.forEach(rawLine -> {
        String pair[] = rawLine.trim().split("=");
        System.setProperty(pair[0], pair[1]);
      });
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}
  • create just one file with env variables required for all the tests: /src/test/resources/application.env
JRE_CACERT_LOCATION=/foo/bar/jre/cacert
JRE_CACERT_PASSWORD=changeme

Dependencies

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.0</version>
    <relativePath />
</parent>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>

Advantages

  • devops compatible
  • run on every shell
  • that file could be used for the developer to configure his Eclipse or Intellij
  • useful to configure manually the application in the server or with some configuration manager
JRichardsz
  • 14,356
  • 6
  • 59
  • 94
0

@Jimmy Praet's modified answer for JUnit5:

@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "classpath:whereever/context.xml")
public class TestWarSpringContext {

    @BeforeAll
    static void initAll() {
        System.setProperty("myproperty", "foo");
    }


    @AfterAll
    static void tearDownAll() {
        System.clearProperty("myproperty");
    }

    ...
}
Radu Dumbrăveanu
  • 1,266
  • 1
  • 22
  • 32