Spring boot how to test WebServerInitialized event - java

I am trying to test functionality in an application that occurs when the app registers with a service registry. This happens only when the app has a full web context (ie. spring-boot-starter-web is on the class path, and the servlet is not mocked). This is controlled via spring-cloud-commons AbstractAutoServiceRegistration.
Simple Test
All the test should do is the following:
1) Bring up Web App
2) Verify auto-registration w/ service registry event fired
3) Manually force close app
4) Verify auto-deregistratoin occurred
Approach 1: #SpringBootTest
SpringBootTest makes it easy to create the full web context, which is great. But I cannot close the app mid-test to force a deregister
#RunWith(SpringRunner.class)
#SpringBootTest(
classes = MyAutoConfig.class,
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT
)
#EnableAutoConfiguration
public class DiscoverySpringCloudBootMinimalRegistrationTest {
#Test
public void register_deregister {
// Force-close app to trigger dereigster (causes exception)
((ConfigurableApplicationContext) context).close();
verify(registry, times(1)).register(autoRegistrationServiceRecord);
verify(registry, times(1)).deregister(autoRegistrationServiceRecord);
}
The context.close() call results in a long error, basically saying not to manually close the context like this.
..... contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]]] is not active. This may be due to one of the following reasons: 1) the context was closed programmatically by user code; 2) the context was closed during parallel test execution either according to #DirtiesContext semantics or due to automatic eviction from the ContextCache due to a maximum cache size policy.
Approach 2: WebContextRunner
In this approach, I avoid #SpringBootTest and manually configure a context runner. This works nicely for calling context.close() but the web context in configures has a mock servlet, and DOES NOT trigger the WebInitializedEvent required for autoregistration.
public class BasicAutoConfigTests {
private WebApplicationContextRunner runner;
#Test
public void register_deregister() {
runner = new WebApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(MyAutoConfig.class));
runner.run((context) -> {
assertThat(context).hasNotFailed();
ServiceRegistry registry = context.getBean(ServiceRegistry.class);
ServiceRecord autoRegistration = context.getBean(MyServiceRecord.class);
context.close();
verify(registry, times(1)).register(autoRegistration);
verify(registry, times(1)).deregister(autoRegistration);
});
}
This almost works but results in a MockServletContext bean, which I presume is failing to trigger the requisite WebServerInitializedEvent from spring-cloud-commons. How can this approach bootstrap an authentic, complete embedded tomcat server?

Following Spencer's advice, I used the spring application builder to create a full web app. I also did this outside of the autoconfiguration module - created a new maven submodule called "integration tests" with spring-boot-starter-web on the classpath.
#Import(MyAutoConfig.class)
#SpringBootApplication
public class MinStarterBasicApp {
#Bean
ServiceRegistry serviceRegistry() {
return mock(ServiceRegistry.class);
}
static ConfigurableApplicationContext setupWebApp(String... profiles){
System.setProperty("spring.main.allow-bean-definition-overriding", "true");
SpringApplication app = new SpringApplicationBuilder(MinStarterBasicApp.class)
.web(WebApplicationType.SERVLET)
.profiles(profiles)
.build();
return app.run();
}
}
Where profiles allows me to pass in a application.properties files by name as shown below. Also it's important to make sure we manually close the app context for each test.
public class StarterBasicAutoconfigTest {
ConfigurableApplicationContext context;
#After
public void teardown() {
if (context != null && context.isRunning())
context.close();
}
#Test
public void sometest() {
context = MinStarterBasicApp.setupWebApp("profile1");
ServiceRegistry registry = context.getBean(ServiceRegistry.class);
context.close();
Mockito.verify(registry, times(1)).register(any());
Mockito.verify(registry, times(1)).deregister(any());
}

Related

Spring Context and Autowire configuration to copy production to test?

I need to write a utility that copies data from a Production to a Test environment to prepare a database for testing the application prior to a large refactoring.
I really need to re-use the existing application config files unchanged, so that I don't find myself refactoring prior to establishing a testing baseline.
My first attempt is to try to instantiate the same beans with different configuration contexts corresponding to each environment.
Here is my first attempt:
public class PrepareDatabase {
#Component
static class Context {
#Autowired UserInfoSecurity userSec;
#Autowired UserDao userDao;
}
public static void main(String[] args) {
try(ClassPathXmlApplicationContext prodContext =
new ClassPathXmlApplicationContext("classpath:/prod/web-client-env.xml");
ClassPathXmlApplicationContext testContext =
new ClassPathXmlApplicationContext("classpath:/test/web-client-env.xml")) {
Context prod = prodContext.getBean(Context.class);
Context test = testContext.getBean(Context.class);
Stream<UserDso> users = prod.userDao.scan().map(prod.userSec::sanitize);
test.userDao.batchPutItems(users);
}
}
}
This fails with No qualifying bean of type '...PrepareDataba se$Context' available
I think I understand what's happening: the PrepareDatabase class is not being scanned.
But how can I fix this? I am not confident that this is even the right approach to begin with.
Got it working by switching to AnnotationConfigApplicationContext.
The relevant section of code looks like this:
try(AnnotationConfigApplicationContext prodContext =
new AnnotationConfigApplicationContext();
AnnotationConfigApplicationContext testContext =
new AnnotationConfigApplicationContext()) {
System.setProperty("config_path", "classpath:/prod");
prodContext.register(MyApp.class);
prodContext.refresh();
System.setProperty("config_path", "classpath:/test");
testContext.register(MyApp.class);
testContext.refresh();
Context prod = prodContext.getBean(Context.class);
Context test = testContext.getBean(Context.class);
YMMV - in my case the config_path variable is used in various places already to import the correct environment-specific files with something like this
<import resource="${config_path}/some_file_name.xml"/>
and this
#PropertySources({#PropertySource("${config_path}/backend.properties"),

#SpringBootTest requiring database connection?

I have some integration tests, for which I am using Testcontainers. But I have suddenly realized that when my docker container with database for my application is down, all the other tests (excluding the integration tests using Testcontainers) are failing (even the contextLoads() test generated by Spring Boot initializr)
I get:
java.lang.IllegalStateException: Failed to load ApplicationContext at
org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:132)
Caused by: org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'liquibase' defined in class path
resource
[org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration$LiquibaseConfiguration.class]:
Invocation of init method failed; nested exception is
liquibase.exception.DatabaseException:
com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications
link failure
It is obvious that the application wants to connect to the database, and the database container is down.
I've been investigating, but I don't remember ever needing to start a container just for the test/build process of an application, so this problem is new for me. But if there is something done wrong, it could be here, in my AbstractDatabaseIT class:
#ActiveProfiles("test")
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
#ContextConfiguration(initializers = AbstractDatabaseIT.DockerMySqlDataSourceInitializer.class)
#Testcontainers
public abstract class AbstractDatabaseIT {
private static final String MYSQL_IMAGE_NAME = "mysql:5.7.24";
public static final MySQLContainer<?> mySQLContainer = new MySQLContainer<>(MYSQL_IMAGE_NAME);
static {
mySQLContainer.start();
}
public static class DockerMySqlDataSourceInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
#Override
public void initialize(#NotNull ConfigurableApplicationContext applicationContext) {
Map<String, String> parameters = new HashMap<>();
parameters.put("command", "--character-set-server=utf8");
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(
applicationContext,
"spring.datasource.url=" + mySQLContainer.getJdbcUrl(),
"spring.datasource.username=" + mySQLContainer.getUsername(),
"spring.datasource.password=" + mySQLContainer.getPassword()
);
mySQLContainer.setParameters(parameters);
}
}
}
The integration test extend this class:
public class ChallengeIT extends AbstractDatabaseIT {
#Autowired
private ChallengeRepository repository;
// tests here
All the other, non-integration classes have #SpringBootTest annotation, and the dependencies injected using #Autowired (maybe this is a problem here?)
#SpringBootTest
class EthMessageVerifierTest {
#Autowired
private EthMessageVerifier ethMessageVerifier;
// tests here
What am I missing here? I remember seeing the H2 database dependency all around many projects. Should I drop the testcontainers in favour of H2? Or can I somehow create a single testcontainer instance for all the other tests?
Tests that you annotate with #SpringBootTest try to populate the entire Spring context. This includes all your beans: your web layer, your business logic, your database setup, etc.
Hence all the infrastructure (e.g. messaging queues, remote systems, databases) that you need otherwise to run your entire application also needs to be present for such tests.
So #SpringBootTest also indicates an integration test and you need to provide your database setup as on application start, Spring Boot's auto-configuration tries to configure your DataSource.
For more information, consider this article on #SpringBootTest and this general overview about unit & integration testing with Spring Boot. You don't always have to use #SpringBootTest and can also use one of Spring Boots many test slice annotations to test something in isolation.

Run method before loading ApplicationContext with SpringJUnit4ClassRunner

I'm a new engineer working on a large legacy system tasked with setting up a series of unit tests making use of the application's existing Spring configuration. This legacy system depends on an additional configuration system called LegacyConfig that pre-dates the adoption of Spring. As the adoption of Spring was built on top of LegacyConfig, many of the bean definitions depend on this configuration system already having been initialized through the use of expressions in XML definitions. This LegacyConfig is typically initialized in our existing unit tests with a single call: LegacyConfig.initialize().
I have my unit test class set up like so:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = {LegacySystemConfig.class, TestClass.BeanConfig.class})
public class TestClass {
#Autowired
SomeTestingDependency someTestingDependency;
#BeforeClass
public static void setup() {
...
}
#Test
public void myTest() {
...
}
....
#Configuration
static class BeanConfig {
#Bean
#Primary
public OutsideServiceClient mockServiceClient() {
return Mockito.mock(OutsideServiceClient.class);
}
}
}
Since we have this dependency on LegacyConfig having been initialized, attempting to run the tests results in a java.lang.IllegalStateException: Failed to load ApplicationContext exception as the Spring configuration attempts to fetch a value from LegacyConfig, receives null instead, and fails to convert null to a primitive boolean.
I've attempted to place the LegacyConfig initialization within the setup() method, but this fails because the ApplicationContext is attempted be loaded before setup() is called, resulting in the exception again.
Is there a way for me to run the LegacyConfig setup before the test runner or ApplicationContext are initialized?

Resolving port already in use in a Spring boot test DEFINED PORT

I have a spring boot application that fires up and executes a class that listens to Application Ready event to call an external service to fetch some data and then use that data to push some rules to the classpath for execution. For local testing we have mocked the external service within our application which works fine during the application startup.
The issue is while testing the application by running it with spring boot test annotation and embedded jetty container either on :
RANDOM PORT
DEFINED PORT
In case of RANDOM PORT, at the application startup, it picks up the url for the mock service from the properties file at a defined port and has no clue where the embedded container is running since it is randomly picked up, hence failing to give response.
In case of DEFINED PORT, for the first test case file it runs successfully, but the moment next file is picked up, it fails saying the port is already in use.
The test cases are partitioned logically in multiple files and need
the external service to be called before the container starts to load
the rules.
How can I either share the embedded container between test files in case of using defined port or refactor my application code instead to get hold of the random port while starting up during the test case execution.
Any help would be appreciated.
Application Startup code :
#Component
public class ApplicationStartup implements ApplicationListener<ApplicationReadyEvent> {
#Autowired
private SomeService someService;
#Override
public void onApplicationEvent(ApplicationReadyEvent arg0) {
try {
someService.callExternalServiceAndLoadData();
}
catch (Execption e) {}
}
}
Test Code Annotations: Test1
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
#TestPropertySource("classpath:test-application.properties")
public class Test1 {
#Autowired
private TestRestTemplate restTemplate;
#Test
public void tc1() throws IOException {.....}
Test Code Annotations: Test2
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
#TestPropertySource("classpath:test-application.properties")
public class Test2 {
#Autowired
private TestRestTemplate restTemplate;
#Test
public void tc1() throws IOException {.....}
If you insist on using the same port on multiple test, you can prevent spring from caching the context for further tests by annotating your testclass with: #DirtiesContext
In your case:
#RunWith(SpringRunner.class)
#DirtiesContext
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
#TestPropertySource("classpath:test-application.properties")
Here is a quote from Andy Wilkinson from his answer on this discussion
This is working as designed. Spring Framework's test framework will, by default, cache contexts for possible reuse by multiple test classes. You have two tests with different configuration (due to #TestPropertySource) so they will use different application contexts. The context for the first test will be cached and kept open while the second test is running. Both tests are configured to use the same port for Tomcat's connector. As a result, when the second test is run, the context fails to start due to a port clash with the connector from the first test. You have a few options:
Use RANDOM_PORT
Remove #TestPropertySource from Test2 so that the contexts have identical configuration and the context from the first test can be reused for the second test.
Use #DirtiesContext so that the context isn't cached
I ran across the same issue. I know this question is a little old, but this may be of assistance:
Tests that use #SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT) can also inject the actual port into a field by using the #LocalServerPort annotation, as shown in the following example:
Source: https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#howto-user-a-random-unassigned-http-port
The code example given is:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
public class MyWebIntegrationTests {
#Autowired
ServletWebServerApplicationContext server;
#LocalServerPort
int port;
// ...
}
in application.properties
server.port=0
will run the application in random ports

Spring Boot, Scheduled task, double invocation

Got a pretty standard Spring Boot (1.3.5) application.
Enabled scheduling with #EnableScheduling (tried on main application entry point and a #Configuration annotated class.
Created a simple class with a #Scheduled method (simple fixedDelay schedule).
Scheduled task executes twice (always).
From what I have gathered so far, it is probably because two contexts are being loaded, and thusly picking up my beans twice.
Ok.
So how do I fix/prevent this double execution, since all the config is basically hidden Spring Boot magic?
Framework versions:
Spring Boot 1.3.5
Spring Cloud Brixton SR1
Main application:
#SpringBootApplication
#EnableDiscoveryClient
#EnableAsync
#EnableCircuitBreaker
public class AlertsApplication {
public static void main(final String[] args) {
SpringApplication.run(AlertsApplication.class, args);
}
}
My task class (HookCreateRequest list is pulled in from application.yml - I do not believe that to be relevant currently, but if required, can be provided):
#ConditionalOnProperty(name = "init.runner", havingValue = "InitRunner")
#ConfigurationProperties(prefix = "webhook")
public class InitRunner /*implements CommandLineRunner*/ {
private final List<HookCreateRequest> receivers = new ArrayList<>();
#Autowired
private WebHookService hookService;
#Scheduled (fixedRate = 300000)
public void run() throws Exception {
getReceivers().stream().forEach(item -> {
log.debug("Request : {}", item);
hookService.create(item);
});
}
public List<HookCreateRequest> getReceivers() {
return receivers;
}
}
There is zero xml configuration.
Not sure what else might be relevant?
EDIT 2016/07/04
I have modified to output the scheduled instance when it runs (I suspected that two different instances were being created). However, the logs seem to indicate it is the SAME instance of the task object.
logs:
15:01:16.170 DEBUG - scheduled.ScheduleHookRecreation - Schedule task running: scheduled.ScheduleHookRecreation#705a651b
...task stuff happening
...first run completes, then:
15:01:39.050 DEBUG - scheduled.ScheduleHookRecreation - Schedule task running: scheduled.ScheduleHookRecreation#705a651b
So it would seem it is the same task instance (#705a651b). Now why would in the name of sweet things would it be executed twice?
EDIT 2016/07/05
I added a #PostConstruct method to the class that carries the scheduled method, with just some logging output in. By doing that I could verify that the #PostConstruct method is being called twice - which seems to confirm that the bean is being picked up twice, which which presumably means it is fed to the scheduler twice. So how to prevent this?
Had the same problem, in my case the reason was in #Scheduled annotation's initialDelay parameter absence - method was called on application start.

Categories