How to inject Environment into ExecutionCondition - spring-boot

I'm writing unit tests and trying to use ExecutionCondition for enabling the test only when specific profile activated exclusively.
I created my ExecutionCondition.
class EnabledWithH2ExclusiveExecutionCondition implements ExecutionCondition {
#Override
public ConditionEvaluationResult evaluateExecutionCondition(
final ExtensionContext context) {
// check the environment
}
#Autowired
private Environment environment;
}
But the environment is not injected.
How can I do that?

Because your ExecutionCondition is created by JUnit5 itself using reflection .It is not managed by Spring and so the #Autowired will not work.
You can call SpringExtension.getApplicationContext() to get Spring Context and then get the Environment from it :
#Override
public ConditionEvaluationResult evaluateExecutionCondition(final ExtensionContext context){
Environment env = SpringExtension.getApplicationContext(context).getEnvironment();
// check the environment
}

Related

Passing an external property to JUnit's extension class

My Spring Boot project uses JUnit 5. I'd like to setup an integration test which requires a local SMTP server to be started, so I implemented a custom extension:
public class SmtpServerExtension implements BeforeAllCallback, AfterAllCallback {
private GreenMail smtpServer;
private final int port;
public SmtpServerExtension(int port) {
this.port = port;
}
#Override
public void beforeAll(ExtensionContext extensionContext) {
smtpServer = new GreenMail(new ServerSetup(port, null, "smtp")).withConfiguration(GreenMailConfiguration.aConfig().withDisabledAuthentication());
smtpServer.start();
}
#Override
public void afterAll(ExtensionContext extensionContext) {
smtpServer.stop();
}
}
Because I need to configure the server's port I register the extension in the test class like this:
#SpringBootTest
#AutoConfigureMockMvc
#ExtendWith(SpringExtension.class)
#ActiveProfiles("test")
public class EmailControllerIT {
#Autowired
private MockMvc mockMvc;
#Autowired
private ObjectMapper objectMapper;
#Value("${spring.mail.port}")
private int smtpPort;
#RegisterExtension
// How can I use the smtpPort annotated with #Value?
static SmtpServerExtension smtpServerExtension = new SmtpServerExtension(2525);
private static final String RESOURCE_PATH = "/mail";
#Test
public void whenValidInput_thenReturns200() throws Exception {
mockMvc.perform(post(RESOURCE_PATH)
.contentType(APPLICATION_JSON)
.content("some content")
).andExpect(status().isOk());
}
}
While this is basically working: How can I use the smtpPort annotated with #Value (which is read from the test profile)?
Update 1
Following your proposal I created a custom TestExecutionListener.
public class CustomTestExecutionListener implements TestExecutionListener {
#Value("${spring.mail.port}")
private int smtpPort;
private GreenMail smtpServer;
#Override
public void beforeTestClass(TestContext testContext) {
smtpServer = new GreenMail(new ServerSetup(smtpPort, null, "smtp")).withConfiguration(GreenMailConfiguration.aConfig().withDisabledAuthentication());
smtpServer.start();
};
#Override
public void afterTestClass(TestContext testContext) {
smtpServer.stop();
}
}
The listener is registered like this:
#TestExecutionListeners(value = CustomTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
When running the test the listener gets called but smtpPort is always 0, so it seems as if the #Value annotation is not picked up.
I don't think you should work with Extensions here, or in general, any "raw-level" JUnit stuff (like lifecycle methods), because you won't be able to access the application context from them, won't be able to execute any custom logic on beans and so forth.
Instead, take a look at Spring's test execution listeners abstraction
With this approach, GreenMail will become a bean managed by spring (probably in a special configuration that will be loaded only in tests) but since it becomes a bean it will be able to load the property values and use #Value annotation.
In the test execution listener you'll start the server before the test and stop after the test (or the whole test class if you need that - it has "hooks" for that).
One side note, make sure you mergeMode = MergeMode.MERGE_WITH_DEFAULTS as a parameter to #TestExecutionListeners annotation, otherwise some default behaviour (like autowiring in tests, dirty context if you have it, etc) won't work.
Update 1
Following Update 1 in the question. This won't work because the listener itself is not a spring bean, hence you can't autowire or use #Value annotation in the listener itself.
You can try to follow this SO thread that might be helpful, however originally I meant something different:
Make a GreenMail a bean by itself:
#Configuration
// since you're using #SpringBootTest annotation - it will load properties from src/test/reources/application.properties so you can put spring.mail.port=1234 there
public class MyTestMailConfig {
#Bean
public GreenMail greenMail(#Value(${"spring.mail.port"} int port) {
return new GreenMail(port, ...);
}
}
Now this configuration can be placed in src/test/java/<sub-package-of-main-app>/ so that in production it won't be loaded at all
Now the test execution listener could be used only for running starting / stopping the GreenMail server (as I understood you want to start it before the test and stop after the test, otherwise you don't need these listeners at all :) )
public class CustomTestExecutionListener implements TestExecutionListener {
#Override
public void beforeTestClass(TestContext testContext) {
GreenMail mailServer =
testContext.getApplicationContext().getBean(GreenMail.class);
mailServer.start();
}
#Override
public void afterTestClass(TestContext testContext) {
GreenMail mailServer =
testContext.getApplicationContext().getBean(GreenMail.class);
mailServer.stop();
}
}
Another option is autowiring the GreenMail bean and using #BeforeEach and #AfterEach methods of JUnit, but in this case you'll have to duplicate this logic in different Test classes that require this behavour. Listeners allow reusing the code.

Can we use #Environment to access a properties at two different places

I 'm trying to access the properties file other than a place where all the application's configuration resides(Datasource,SessionFactory,TransactionManager), hence I'm trying to use #Environment twice in my application.
I'm trying to load a properties file to implement a email notification functionality to access the properties file content using env by auto wiring. But I'm failing while reading the property file using env.getProperty("email.smtp.host")); Getting null in the place of property value when executing the app.
Actually, I'm trying to use two Configuration file, is this what blocks me to autowire Environment property ? I tried to replace with #Component and adding email package to componentscan list as well. I'm thinking that, is Environement property applicable only to #Configuration file ? Can someone help me to debug the issue please or what I'm missing here to initiate the Environment ?
I tried to resolve this by implementing EnvironmentAware interface as below to get the property value using load() but that still didn't help me.
Tried to use PropertySourcesPlaceholderConfigurer as below.
#Bean
public static PropertySourcesPlaceholderConfigurer propertyPlaceHolderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
#Autowired
private Environment env;
#Configuration
#PropertySource("classpath:email.properties")
public class PropertiesUtil implements EnvironmentAware {
#Autowired
private Environment environment;
public void sendPlainTextEmail(String mailMessage) throws AddressException, MessagingException{
try {
// Configure SMTP
Properties properties = new Properties();
properties.setProperty("mail.smtp.host",this.env.getProperty("email.smtp.host"));
properties.setProperty("mail.smtp.port", "25");
}
#Override
public void setEnvironment(final Environment environment) {
this.environment = environment;
}
#Bean
public String load(String propertyName)
{
return environment.getRequiredProperty(propertyName);
}
}
Expecting to read the email property but getting only Null and no other information to debug.

How to set active profile programmatically for any environment?

I want to set active profile host dependent for any envrionment and cannot find an environment independent hook.
Following factory will set the active profile before application context will build.
/META-INF/spring.factories
org.springframework.context.ApplicationContextInitializer=MyApplicationContextInitializer
MyApplicationContextInitializer.java
public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
#Override
public void initialize(ConfigurableApplicationContext ca) {
ConfigurableEnvironment environment = ca.getEnvironment();
environment.addActiveProfile("myHost");
}
}
If this example is executed in a mock environment by JUnit ...
*Test.java
...
#RunWith(SpringRunner.class)
#SpringBootTest
#AutoConfigureMockMvc
...
... following will be logged.
...
... RestControllerTests : The following profiles are active: myHost
...
but profile myHost is not active and default profile will be used in context of JUnit!
A test as Java Application and JUnit with VM arguments works ...
-Dspring.profiles.active=myHost
I use a war packaged spring-boot-starter-web app and programmatically profile shall be set and used in any environment
Java Application
JUnit
Servlet Container
How do I set the profile programmatically for any environmnet?
I do not want to use VM arguments or environemt variables because the profile shall be set by the current host name.
Simplest answer after a lot of googling :)
#SpringBootApplication
public class ExampleMain2 {
public static void main(String[] args) {
ConfigurableEnvironment environment = new StandardEnvironment();
environment.setActiveProfiles("dev");
SpringApplication sa = new SpringApplication(ExampleMain2.class);
sa.setEnvironment(environment);
sa.setAdditionalProfiles("remote","live");
sa.run(args);
}
}
I had the same issue and I finally solved it implementing the ActiveProfilesResolver interface.
In your case you could do something like this:
public class MyActivateProfilesResolver implements ActiveProfilesResolver {
#Override
public String[] resolve(Class<?> testClass) {
// some code to find out your active profiles
return new String[] {"myHost"};
}
}
And then you need to link your test with your resolver like this:
#ActiveProfiles(resolver = MyActivateProfilesResolver.class)
I don't think there is such a thing as setting active profile dynamically (programmatically) at runtime once the application is running, any modification to the profile will not have any effect on the loaded beans and properties files.
However, before running the application you can configure the available profiles of the application. for example:
#SpringBootApplication
public class DemoApplicationWithSysProperty {
public static void main(String[] args) {
System.setProperty(AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME,
PROFILE_NAME);
SpringApplication.run(DemoApplicationWithSysProperty.class, args);
}
}
Don't you think that could be achieved with an EnvironmentPostProcessor ans still have an impact on the running environment? My attempts failed.
#Order(Ordered.LOWEST_PRECEDENCE)
public class EnvtPostProcessor implements EnvironmentPostProcessor {
#SuppressWarnings("deprecation")
#Override
public void postProcessEnvironment(ConfigurableEnvironment env, SpringApplication application) {
if ((env.acceptsProfiles("bar1") || env.acceptsProfiles("bar2"))
&& !(env.acceptsProfiles("foo1") || env.acceptsProfiles("foo2"))) {
env.addActiveProfile("foo1");
}
}
}
NB: don't forget to register the processor in src/main/resources/META-INF/spring.factories:
org.springframework.boot.env.EnvironmentPostProcessor=com.mygroup.myapp.EnvtPostProcessor
When I only apply the "bar2" active profile, the "foo1" profile is added to the environnement as an active profile, before application context initialization; but not all beans are found eventually (as they are in the originally complete active profiles list use case).
Maybe that's because of the dependencies (Azure for Spring Cloud connection, by the way) : I end up with no OAuth2UserService found for my ActiveDirectoryConfigurer.

Throwing an Exception to Terminate Spring Application Startup

My Spring application depends on certain environment variables that needs to have been set before application launch.
For eg. consider the following controller:
#Controller
public class FileUploadController {
/** Path to which all data will be uploaded **/
private Path appDataPath;
public FileUploadController(){
// Extract the App Data path from environment variables
Map<String, String> environmentVariables = System.getenv();
if (environmentVariables.containsKey("MYAPP_DATA_DIR")) {
String dataPath = environmentVariables.get("MYAPP_DATA_DIR");
appDataPath = Paths.get(dataPath);
} else {
// TODO: Throw an exception to terminate app
}
}
}
What exception do I need to throw in the code above to terminate application startup?
You are making things to complex, either simply inject a String for the path and annotate that with #Value or inject the Environment and use getRequiredProperty either of them will automatically kill the startup of the application.
#Controller
public class FileUploadController {
#Value("${MYAPP_DATA_DIR}"
private String dataPath;
private Path appDataPath;
#PostConstruct
public void init() {
appDataPath = Paths.get(dataPath);
}
}
Or simply use the Environment abstraction in a #PostConstruct method.
#Controller
public class FileUploadController {
#Autowired
private Environment env;
private Path appDataPath;
#PostConstruct
public void init() {
appDataPath = Paths.get(env.getRequiredProperty("MYAPP_DATA_DIR"));
}
}
Both will automatically blow up when the property isn't defined.
Spring Framework is an implementation of Java Enterprise Container!
By definition of the Java Enterprise Containers, you need to throw subclass of RuntimeException! They are not required to be handled by the container or the calling code.
In case your mandatory environment variables not set and your application cannot start at all then you need to notify and exit.
I would create a SubClass exception from the RuntimeException and use that Exception class!
In case your spring Framework implementation has some strict requirements around (i.e. some Spring subclass of the RuntimeException, ApplicationException ...) then you can subclass that class. But I do not think that Spring is restrictive in this subject...

How to active profile as environment variable

I was configuring environment variables using spring profiles in my spring boot application. There i did configuration like
My interface is
public interface EnvConfiguration {
String getServerUrl();
}
My development configuration is
#Component
public class DevelopmentConfig implements EnvConfiguration{
#Value("${DEV}")
private String serverUrl;
#Override
public String getServerUrl(){
return serverUrl;
}
}
#Configuration
#Profile("dev")
public class DevelopmentProfile {
#Bean
public EnvConfiguration getDevelopmentConfig(){
return new DevelopmentConfig();
}
}
Same as i did configured for production environment
#Component
public class ProductionConfig implements EnvConfiguration {
#Value("${PROD}")
private String serverUrl;
#Override
public String getServerUrl(){
return serverUrl;
}
}
#Configuration
#Profile("prod")
public class ProductionProfile {
#Bean
public EnvConfiguration getProductionConfig(){
return new ProductionConfig();
}
}
Now i configured environment variable in eclipse using run configurations->agruments
-Dspring.profiles.active="dev"
Now when i trying to run my application,i am getting error:
expected single matching bean but found 2: productionConfig,developmentConfig
So please help me what am i missing there ?
Thanks in advance!
I was adding programming arguments,we have to add vm arguments
Why are you trying to configure environment properties with Java ?
You could put all your configuration into an application.properties.
Then if you want dev environment, you juste override the properties you want in application-dev.properties.
The same for prod in application-prod.properties.
Then you start as you did with -Dspring.profiles.active=dev and you will be able to retrieve value with #Value.

Resources