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

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.

Related

Write Unit test in SpringBoot Without start application

Am developing MicroServices in springBoot. Am writing unit test for Service and DAO layer. When I use #SpringBootTest it starting application on build. But It should not start application
when I run unit test. I used #RunWith(SpringRunner.class), But am unable to #Autowired class instance in junit class. How can I configure junit test class that should not start application and how to #Autowired class instance in junit class.
Use MockitoJUnitRunner for JUnit5 testing if you don't want to start complete application.
Any Service, Repository and Interface can be mocked by #Mock annotation.
#InjectMocks is used over the object of Class that needs to be tested.
Here's an example to this.
#RunWith(MockitoJUnitRunner.class)
#SpringBootTest
public class AServiceTest {
#InjectMocks
AService aService;
#Mock
ARepository aRepository;
#Mock
UserService userService;
#Before
public void setUp() {
// MockitoAnnotations.initMocks(this);
// anything needs to be done before each test.
}
#Test
public void loginTest() {
Mockito.when(aRepository.findByUsername(ArgumentMatchers.anyString())).thenReturn(Optional.empty());
String result = aService.login("test");
assertEquals("false", result);
}
With Spring Boot you can start a sliced version of your application for your tests. This will create a Spring Context that only contains a subset of your beans that are relevant e.g. only for your web layer (controllers, filters, converters, etc.): #WebMvcTest.
There is a similar annotation that can help you test your DAOs as it only populates JPA and database relevant beans (e.g. EntitiyManager, Datasource, etc.): #DataJpaTest.
If you want to autowire a bean that is not part of the Spring Test Context that gets created by the annotatiosn above, you can use a #TestConfiguration to manually add any beans you like to the test context
#WebMvcTest(PublicController.class)
class PublicControllerTest {
#Autowired
private MockMvc mockMvc;
#TestConfiguration
static class TestConfig {
#Bean
public EntityManager entityManager() {
return mock(EntityManager.class);
}
#Bean
public MeterRegistry meterRegistry() {
return new SimpleMeterRegistry();
}
}
}
Depending your test setup, if you don't want to autowire a mock but the "real thing", You could simply annotate your test class to include exactly the classes you need (plus their transitive dependencies if necessary)
For example :
#SpringJUnitConfig({ SimpleMeterRegistry.class })
or
#SpringJUnitConfig
#Import({ SimpleMeterRegistry.class })
or
#SpringJUnitConfig
#ContextConfiguration(classes = { SimpleMeterRegistry.class })
See working JUnit5 based samples in here Spring Boot Web Data JDBC allin .

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.

Where should I place the application logic of non-web, non-batch Spring Boot app?

I'm trying to start my non-web/non-batch Spring boot application properly.
However, if I place the main tasks in a CommandLineRunner it also gets triggered while running the tests. Running the tasks as batch job will work, but my task doesn't follow batch job semantics.
Is extending SpringApplication class and putting the logic in run() method after super() call the standard way?
If you annotate your CommandLineRunner bean with the #Profile annotation, you can tell Spring to only create the bean when running with (or without) certain profiles, for example:
#Component
#Profile("autorun")
public class JobRunner implements CommandLineRunner {
// ...
}
As long as you don't use those profile when testing, it should not be invoked. You can then run the application using the -Dspring.profiles.active=autorun parameter.
You can create a separate Component class and assign a profile
#Component
#Profile("!test")
public class RunApplication implements CommandLineRunner {
#Override
public void run(String... args) throws IOException {
//Your code here
}
}
This class will only be initialized when the spring.profiles.active variable is not equal to test
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-profiles.html
https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/CommandLineRunner.html

SpringBootTest: how to know when boot application is done

Spring boot integration test looks like this
#RunWith(SpringRunner.class)
#SpringBootTest(classes = Application)
class IntegrationTest {
static QpidRunner qpidRunner
#BeforeClass
static void init() {
qpidRunner = new QpidRunner()
qpidRunner.start()
}
#AfterClass
static void tearDown() {
qpidRunner.stop()
}
}
So, Qpid instance is run before and teared down after all tests. I want to know is there a way to check whether spring boot application is still running before calling qpidRunner.stop(). I want to stop Qpid only when I'm sure that spring app has finished its stopping.
The Spring Boot integration test can configure an ApplicationListener which listens for ContextClosedEvent. Define a nested #TestConfiguration class inside the test class to add beans to the application's primary configuration.
#TestConfiguration
static class MyConfiguration {
#Bean
public ApplicationListener<ContextClosedEvent> contextClosedEventListener() {
return event -> qpidRunner.stop();
}
}
Taking into account that ConfigurableWebApplicationContext can be injected in a SpringBootTest, adding this lines to the code solved the problem
static ConfigurableWebApplicationContext context
#Autowired
void setContext(ConfigurableWebApplicationContext context) {
AbstractDocsIntegrationTest.context = context
}
#AfterClass
static void tearDown() {
context.stop()
qpidRunner.stop()
}
Spring docs about stop method
Stop this component, typically in a synchronous fashion, such that the
component is fully stopped upon return of this method.
JUnit AfterClass annotated method must be static, therefore #Autowired workaround with setContext method.

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