Nested configuration not working as expected - spring-boot

I'm working on a project using spring boot, and we've just upgraded to version 1.4.0.RELEASE. As part of the version upgrade, we've started using the #SpringBootTest annotation on our abstract integration test class. After reading the documentation, it sounds like we should be able to use a nested #TestConfiguration-annotated config class to override bean definitions during specific tests. This is not working for us, and instead the non-test bean we are trying to override is still being used.
Interestingly, it seems like usages of the mock test bean and the production bean are actually intertwined within the same test, as if both beans exist side by side in the application context. Also, it seems like the order in which the integration tests run somehow affects this behaviour. I'm wondering if this is something we've misconfigured, or if there is something else going on.
edit:
The abstract class that the integration tests inherit from looks like this:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#ActiveProfiles({"local", "test"})
public abstract class BaseIntegrationTest {
#Value("${local.server.port}")
protected int port;
}
The integration test that we are seeing the strange behaviour in looks like this:
public class WebhookProcessorIT extends BaseIntegrationTest {
#TestConfiguration
public static class Config {
#Bean
#Primary
public WebhookTask webhookTask() {
return mock(WebhookTask.class);
}
}
// sometimes the mock above is used and sometimes
// the actual production bean is used
#Autowired
private WebhookTask task;
#Before
public void setup() {
when(task.process(any())).thenReturn(true);
}
// tests ...
}
And this is what the root application context class looks like:
#EnableAutoConfiguration(exclude = ErrorMvcAutoConfiguration.class)
#SpringBootApplication
public class Application {
private static final Logger log = LoggerFactory.getLogger(Application.class);
public static void main(String[] args) {
final SpringApplication app = new SpringApplication(Application.class);
app.setBannerMode(Banner.Mode.OFF);
app.run(args);
}
}
edit:
I have also tried using #MockBean, like so:
public class WebhookProcessorIT extends BaseIntegrationTest {
#MockBean
private WebhookTask task;
but I get the same result when the tests run. I can see that Spring is trying to override the production bean with the mock I am providing when I look at the logs during the test setup:
build 15-Sep-2016 09:09:24 2016-09-15 09:09:24 [34mINFO [0;39m [36m[DefaultListableBeanFactory][0;39m (main) Overriding bean definition for bean 'productionWebhookTask' with a different definition
however when it comes to test execution, i can still see the production bean being used:
build 15-Sep-2016 09:09:29 2016-09-15 09:09:29 [39mDEBUG[0;39m [36m[WebhookSupplier][0;39m (WebhookProcessor) Received webhook with ID '1234' from queue.
build 15-Sep-2016 09:09:30 2016-09-15 09:09:30 [39mDEBUG[0;39m [36m[WebhookSupplier][0;39m (WebhookProcessor) Received webhook with ID '5678' from queue.
build 15-Sep-2016 09:09:30 2016-09-15 09:09:30 [39mDEBUG[0;39m [36m[ProductionWebhookTask][0;39m (WebhookProcessor) Received webhook with ID '1234' for processing // production webhook task bean still being used for webhook '1234'
build 15-Sep-2016 09:09:30 2016-09-15 09:09:30 [39mDEBUG[0;39m [36m[WebhookSupplier][0;39m (WebhookProcessor) Deleting webhook with id '5678' from queue. // mock bean causes production logic to be skipped and we just delete webhook '5678'
// More logs from production webhook task operating on webhook with id '1234' and causing the test to fail

Anyway, you can annotate your test beans with #Profile("test") and your real ones with #Profile("production")
and in your properties file put the property spring.profiles.active=test
from documentation
Unlike regular #Configuration classes the use of #TestConfiguration
does not prevent auto-detection of #SpringBootConfiguration.
Unlike a nested #Configuration class which would be used instead of a
your application’s primary configuration, a nested #TestConfiguration
class will be used in addition to your application’s primary
configuration.

Since you use Spring Boot version 1.4.0, you can go on use the new introduced annotation #MockBean instead of using different configuration classes to mock your original beans. It's straight forward and very suitable to your use case.
Here you go with an example from the documentation

Related

How to exclude Mongo spring data configuration only for tests

I am doing a simple sample project with Spring boot and data.
#Configuration
public class MongoConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(MongoConfig.class);
#Value("${mongo.uri}")
private String mongoUri;
public #Bean MongoClient mongoClient() {
LOGGER.info(" creating connection with mongodb with uri [{}] ", mongoUri);
return MongoClients.create(mongoUri);
}
}
This works fine and connects to mongo on startup. However, the tests also pick this up in autoscan. What is the best practice to make sure that mongo config gets excluded for tests?
If I add #WebMvcTest to the tests, it works. But not all tests will be mvc tests. I might be testing a utility class.
If I try using profiles, it gives my an error java.lang.IllegalStateException: The following classes could not be excluded because they are not auto-configuration classes: ...MongoConfig
#SpringBootTest
#ActiveProfiles("test")
class ApplicationTests {
#Test
void contextLoads() {
}
}
Please tell me a repeatable practice as I will be using it for all my tests.
The simplest way of doing it is to exclude your configuration bean when your test profile is active
#Profile("!test")
#Configuration
public class MongoConfig {
...
}
Here you tell to the BeanFactory to not create this bean if the profile test is present
Ref: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Profile.html

Test, if I don't want to trigger the whole thing

A Spring Boot application
#SpringBootApplication
#EnableScheduling
#Slf4j
public class MyApplication {
#Autowired
private ApplicationEventPublisher publisher;
...
#Bean
public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
...
// read data from a file and publishing an event
}
}
For an integration test, I have something typical.
#SpringBootTest
public class TestingMyApplicationTests{
...
}
After I start a test case in the class, the whole chain events occurs, that is reading a file, publishing events and an event listener acts accordingly.
What is the best approach to avoid such chain events occur during running a test?
If you want to avoid that the whole Spring Context is started for all of your integration tests, you can take a look at other test annotations that create a sliced context:
#WebMvcTest creates a Spring Context with only MVC related beans
#DataJpaTest creates a Spring Context with only JPA/JDBC related beans
etc.
In addition to this, I also would remove your CommandLineRunner from your main entry Spring Boot entry point class. Otherwise also the annotations above would trigger the logic.
Therefore you can outsource it to another #Component class:
#Component
public class WhateverInitializer implements CommandLineRunner{
#Autowired
private ApplicationEventPublisher publisher;
// ...
#Override
public void run(String... args) throws Exception {
...
// read data from a file and publishing an event
}
}
Apart from this you can also use #Profile("production") on your Spring beans to only populate them when a specific profile is active. This way you can either include or exluce them for all your integration tests if you don't want e.g. this startup logic always.

Why DirtiesContext is needed on other test classes to mock bean dependency for class with JMS Listener

Context
A Spring Boot application with a Rest endpoint and a JMS AMQ Listener
Test behaviour observed
The tests classes run fine without needing DirtiesContext individually but when the entire suite of test classes are run the following behaviours are observed -
Mocking of a bean dependency for the JMS Consumer test requires the earlier test classes to have a DirtiesContext annotation.
Mocking of bean dependency for RestControllers seem to work differently than a JMS Listener i.e don't need DirtiesContext on the earlier test classes
I've created a simple Spring application to reproduce the Spring context behaviour I need help understanding - https://github.com/ajaydivakaran/spring-dirties-context
The reason this happens is due to the fact that without #DirtiesContext Spring will remain the context for reuse for other tests that share the same setup (read more on Context Caching in the Spring documentation). This is not ideal for your setup as you have a messaging listener, because now multiple Spring Contexts can remain active and steal the message you put into the queue using the JmsTemplate.
Using #DirtiesContext ensures to stop the application context, hence this context is not alive afterward and can't consume a message:
from #DirtiesContext:
Test annotation which indicates that the {#link org.springframework.context.ApplicationContext ApplicationContext} *
associated with a test is dirty and should therefore be
closed and removed from the context cache.
For performance reasons, I would try to not make use of #DirtiesContext too often and rather ensure that the JMS destination is unique for each context you launch during testing. You can achieve this by outsourcing the destination value to a config file (application.properties) and randomly populate this value e.g. using a ContextInitializer.
A first (simple) implementation could look like the following:
#AllArgsConstructor
#Service
public class Consumer {
private EnergeticGreeter greeter;
private MessageRepository repository;
private ApplicationContext applicationContext;
#JmsListener(destination = "${consumer.destination}")
public void consume(
#Header(name = JmsHeaders.MESSAGE_ID, required = false) String messageId,
TextMessage textMessage) {
System.out.println("--- Consumed by context: " + applicationContext.toString());
if ("Ahem hello!!".equals(greeter.welcome().getContent())) {
repository.save();
}
}
}
the corresponding test:
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#ContextConfiguration(initializers = DestinationValueInitializer.class)
public class JMSConsumerIntegrationTest {
#Autowired
private JmsTemplate jmsTemplate;
#Value("${consumer.destination}")
private String destination;
#Autowired
private ApplicationContext applicationContext;
#MockBean
private EnergeticGreeter greeter;
#MockBean
private MessageRepository repository;
//Todo - To get all tests in this project to pass when entire test suite is run look at Todos added.
#Test
public void shouldInvokeRepositoryWhenGreetedWithASpecificMessage() {
when(greeter.welcome()).thenReturn(new Message("Ahem hello!!"));
System.out.println("--- Send from context: " + applicationContext.toString());
jmsTemplate.send(destination, session -> session.createTextMessage("hello world"));
Awaitility.await().atMost(10, TimeUnit.SECONDS).untilAsserted(
() -> verify(repository, times(1)).save()
);
}
}
and the context initializer:
public class DestinationValueInitializer implements
ApplicationContextInitializer<ConfigurableApplicationContext> {
#Override
public void initialize(ConfigurableApplicationContext applicationContext) {
TestPropertyValues.of("consumer.destination=" + UUID.randomUUID().toString()).applyTo(applicationContext);
}
}
I've provided a small PR for your project where you can see this in the logs, that a different application context is consuming your message and hence you can't verify that the repository was called on the application context you write your test in.

How to ignore ContextRefreshedEvent when SpringBootTest?

I'm struggling to find out how to ignore a class method, which should start a thread when SpringBootApplication is ready, during normal operation:
#EventListener
public void onApplicationEvent(ContextRefreshedEvent event) {
this.start();
}
in case of testing I do not want such behavior, want to start it from test method.
As far as i understand ContextRefreshedEvent is triggered by #SpringBootTest annotation on test class.
For testing the listener itself:
You do not need #SpringBootTest for every test of Spring Boot application (I would argue that you want minimum of such tests actually as they load everything.)
There are other options:
If you don't need anything from Spring: Unit test the service with Mockito (if it has dependencies you want to mock away).
Otherwise: Use slices - for example #JsonTest will autoconfigure ObjectMapper and other beans for working with JSON. There's quite a few of them so check documentation if there's any part of application you would like to be autoconfigured for your test.
For excluding the listener from other #SpringBootTest tests:
I see two options:
Mocking the listener bean away using #MockBeans.
#SpringBootTest
#MockBeans(#MockBean(Listener.class))
public class SomeTest {
// ...
}
Executing tests inside a specific profile and mark the listener bean to be included only inside the default profile. (Or not inside the "test" profile.)
#Component
#Profile("default") // OR: #Profile("!test")
public class Listener {
// ...
}
#SpringBootTest
#ActiveProfiles("test")
public class SomeTest {
// ...
}
You might need to extract the listener from the existing bean if you need the bean as a dependency for another service though.

Does #WebMvcTest require #SpringBootApplication annotation?

My goal is to migrate a Spring Boot application previously developed with Spring Boot 1.3 to the newest Spring Boot version 1.4. The application consists of several maven modules and only one of them contains class annotated with #SpringBootApplication.
One part of migration is to use #WebMvcTest annotation to efficiently test controllers, and here I get an issue.
Consider an example application from Spring Boot github page. #WebMvcTest annotation works perfectly, because, as far as I understand (after I did several tests), there is a class in the main package annotated with #SpringBootApplication. Note that I follow the same concept as shown in the example above for my own #WebMvcTest tests.
The only difference I see that in my application, controller classes are located in a separate maven module (without #SpringBootApplication annotated class), but with #Configuration and SpringBootConfiguration configurations. If I do not annotate any class with #SpringBootApplication I always get an assertion while testing controller. My assertion is the same as when SampleTestApplication class in the example above modified to have only #EnableAutoConfiguration and #SpringBootConfiguration annotations (#SpringBootApplication is not present):
getVehicleWhenRequestingTextShouldReturnMakeAndModel(sample.test.web.UserVehicleControllerTests) Time elapsed: 0.013 sec <<< FAILURE!
java.lang.AssertionError: Status expected:<200> but was:<404>
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:54)
at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:81)
at org.springframework.test.web.servlet.result.StatusResultMatchers$10.match(StatusResultMatchers.java:664)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:171)
at sample.test.web.UserVehicleControllerTests.getVehicleWhenRequestingTextShouldReturnMakeAndModel(UserVehicleControllerTests.java:68)
How should I deal with that? Should I always have class annotated with #SpringBootApplication in order to run #WebMvcTest tests?
EDIT 1: I did a small maven project with 2 modules and a minimal configuration. It is here. Now, I get NoSuchBeanDefinitionException exception for repository defined in another module. If I configure "full" #SpringBootApplication - everything is fine.
EDIT 2: I modified small test project from EDIT 1 to give an original issue. I was playing with different annotations and added #ComponentScan on configuration class, because I suspected that beans are not registered properly. However, I expect that only #Controller bean (defined in #WebMvcTest(...class)) shall be registered based on magic behind #WebMvcTest behaviour.
EDIT 3: Spring Boot project issue.
Short answer: I believe so.
Long answer:
I believe #WebMvcTest needs to find the SpringBootApplication configuration since WebMvcTest's sole purpose is to help simplify tests (SpringBootApplication would rather try to load the whole world).
In your specific case, since you don't have any in your non-test packages, I believe it also finds SampleTestConfiguration which is annotated with #ScanPackages and somehow loads every beans.
Add the following in src/main/java/sample/test
#SpringBootApplication
public class SampleTestConfiguration {
}
And change your test to this:
#RunWith(SpringRunner.class)
#WebMvcTest(MyController.class)
public class MyControllerTest {
#Autowired
private MockMvc mvc;
#MockBean
private MyService ms;
#Autowired
private ApplicationContext context;
#Test
public void getDataAndExpectOkStatus() throws Exception {
given(ms.execute("1")).willReturn(false);
mvc.perform(get("/1/data").accept(MediaType.APPLICATION_JSON_VALUE)).andExpect(status().isOk()).andExpect(content().string("false"));
}
#Test
public void testMyControllerInAppCtx() {
assertThat(context.getBean(MyController.class), is(not(nullValue())));
}
#Test
public void testNoMyAnotherControllerInAppCtx() {
try {
context.getBean(MyAnotherController.class);
fail("Bean exists");
} catch (BeansException e) {
// ok
}
}
}
#WebMvcTest finds the SpringBootApplication, then load only a limited number of beans (see documentation):
#WebMvcTest will auto-configure the Spring MVC infrastructure and
limit scanned beans to #Controller, #ControllerAdvice, #JsonComponent,
Filter, WebMvcConfigurer and HandlerMethodArgumentResolver. Regular
#Component beans will not be scanned when using this annotation.
WebMvcTest requires SpringBootApplication: WebMvcTest inherits many AutoConfiguration, so it needs SpringBoot to load them. Then it disables many other AutoConfiguration and your Controllers become easily testable.
The whole point of using WebMvcTest is when you have a SpringBootApplication and you wish to make it simpler to test by disabling all beans except Controllers. If you don't have SpringBootApplication, then why use WebMvcTest at all?
It's an old topic, but there is a solution which wasn't mentioned here.
You can create a class annotated with SpringBootApplication just in your test sources. Then, you still have a nice, multi-module structure of your project, with just one "real" SpringBootApplication.
Yes,according to the spring boot docs
The search algorithm works up from the package that contains the test until it finds a #SpringBootApplication or #SpringBootConfiguration annotated class. As long as you’ve structure your code in a sensible way your main configuration is usually found.
But after I started using #WebMvcTest,spring boot still try to load other beans, finally TypeExcludeFilter did the trick.
#RunWith(SpringRunner.class)
#WebMvcTest(controllers = {JzYsController.class} )
public class JzYsControllerTest {
private static final String REST_V4_JZYS = "/rest/v4/JzYs";
#Autowired
private MockMvc mockMvc;
#MockBean
private JzYsService service;
#Test
public void deleteYsByMlbh() throws Exception {
Mockito.when(service.deleteYsByMlbh(Mockito.anyString())).thenReturn(Optional.of(1));
mockMvc.perform(delete(REST_V4_JZYS + "?mbbh=861FA4B0E40F5C7FECAF09C150BF3B01"))
.andExpect(status().isNoContent());
}
#SpringBootConfiguration
#ComponentScan(excludeFilters = #Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class))
public static class config{
}
}
There is one more solution. You can not use #WebMvcTest, but configure MockMvc yourself through the builder
class TestControllerTest {
private MockMvc mvc;
#BeforeEach
public void setup() {
mvc = MockMvcBuilders.standaloneSetup(new TestController())
.build();
}
#Test
void test() throws Exception {
// When
var res = mvc.perform(MockMvcRequestBuilders.get("/test/test"));
// Then
res.andExpect(status().isOk());
}
}
But this solution may entail a number of other problems, such as problems with configurations, environment property injections, etc.

Resources