4

For the integration tests of my Spring Boot app, I have declared custom meta-annotations (a bit like Spring Boot's test slice annotations). How can I declare different TestExecutionListeners in each meta-annotation, and have all of them merged when running a test class?

I can only find mergeMode = MERGE_WITH_DEFAULTS which merges the declared TestExecutionListener with the default ones, but not different custom Listeners declared in different places.

A minimal example:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@TestExecutionListeners(
  listeners = DbTestListener.class,
  mergeMode = MERGE_WITH_DEFAULTS)
public @interface DbIntegrationTest {
}


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@TestExecutionListeners(
  listeners = MessagingTestListener.class,
  mergeMode = MERGE_WITH_DEFAULTS)
public @interface MessagingIntegrationTest {
}


@RunWith(SpringRunner.class)
@DbIntegrationTest
@MessagingIntegrationTest
public class ExampleTest {
  // test cases here
}

As a result, I'd like to have both custom TestListeners and the default ones executed for my ExampleTest.

Clarification: the above example is minimal as to show what I want. Of course like that it doesn't make much sense. My own composed annotations have much more setup in them which I haven't shown, and I have multiple layers of meta-annotations.

jhyot
  • 3,733
  • 1
  • 27
  • 44

3 Answers3

2

There is no such features in the current version of spring test.

If you think more deeply about your ideas, it will introduce ambiguous in some cases. For example, if @DbIntegrationTest and @MessagingIntegrationTest are configured with different values of mergeMode or inheritListeners, which value should the framework use?

The current behaviour is that if there are multiple @TestExecutionListeners or its meta-annotation are marked , only the topmost declared one will take effect while the other will be ignored.

Some ideas which may help you to achieve the similar result :

  1. Use a meta-annotation to predefine some kind of profile which groups several TestExecutionListeners that will be used together in different test scenario. Just annotate this single meta-annotation to the test class. For example :

@MessagingTest contains all the required listeners for messaging integration test :

@TestExecutionListeners(
  listeners = {SetupMessageBrokerListener.class, FooBarListener.class } ,
  mergeMode = MERGE_WITH_DEFAULTS)
public @interface MessagingTest {

}

@DatabaseTest contains all the required listeners for DB integration test :

@TestExecutionListeners(
  listeners = {SetupDatabaseListener.class, CreateTestingDataListener.class , FooBarListener.class } ,
  mergeMode = MERGE_WITH_DEFAULTS)
 public @interface DatabaseTest{ 

 }
  1. Implement your own version of TestContextBootstrapper. What TestExecutionListener to be created is defined in its getTestExecutionListeners(). Use @BootstrapWith to activate this TestContextBootstrapper. AbstractTestContextBootstrapper is the good starting point . Basically , you need to add your customised logic at the end of getTestExecutionListeners() . Sadly, this method is finalised which does not allow to override in the sub-classes...
Ken Chan
  • 84,777
  • 26
  • 143
  • 172
2

A possible solution is to register all TestExecutionListeners in META-INF/spring.factories, and write each Listener so that it only does something if some annotation (or other marker) is present on the test class or in the test context.

For usage of spring.factories see: https://docs.spring.io/spring/docs/current/spring-framework-reference/testing.html#testcontext-tel-config-automatic-discovery

The disadvantage of this solution is that the setup of TestExecutionListeners (which are defined and which will be active) is a bit "hidden" or spread out in the code base.

jhyot
  • 3,733
  • 1
  • 27
  • 44
  • Good try , should be okay . In this case , you have to override `getDefaultTestExecutionListenerClasses()` of `DefaultTestContextBootstrapper` , and add these logic to it. – Ken Chan Sep 05 '19 at 11:18
  • No actually I think the standard bootstrapper does this already. The Spring documentation says explicitly: "Third-party frameworks and developers can contribute their own TestExecutionListener implementations to the list of default listeners in the same manner through their own META-INF/spring.factories properties file." – jhyot Sep 05 '19 at 12:52
  • i mean after you register your deafult listeners, you still need to override this method to remove some of these loaded listener based on your annotation logic. i doubt @TestExecutionListener will not have any effect on these default listener , it just simply load all of them – Ken Chan Sep 05 '19 at 13:19
0

What do these listeners do? If provide execution environment then you misuse listener API.

To configure (and cleanup) environment you should use

  • @Rule \ @ClassRule if you use Junit4
  • @ExtendWith if you use Junit5

These are easily combined within single test.

Alexander Pavlov
  • 2,264
  • 18
  • 25
  • They do preparation and/or postprocessing of the testing environment. The DbListener e.g. clears the DB before each test. They are an integral part of Spring's test-framework-independent testing support, see: https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/test/context/TestExecutionListener.html – jhyot Sep 03 '19 at 06:06
  • As for why not using e.g. `@Rule`: The `@TestExecutionListeners` annotation can be used as a meta-annotation (i.e. in composed annotations) as I've shown in my example. With `@Rule` etc, I'd have to add to rule to each test class. With my `@DbIntegrationTest` composed annotation, I can just put that annotation on top of my class and everything is handled from there (cohesiveness of test setup). Additionally, the Spring TestExecutionListeners have access to Spring's test context which can be useful. – jhyot Sep 03 '19 at 06:11
  • To be honest, I do not see difference between `@ClassRule` and meta-annotation in your case: meta annotation should be added once per test ("on top of my class") and `@ClassRule` should be added once per test ("I'd have to add to rule to each test class"). Anyway, I only can advise you to try latest Spring framework - probably they improved it and if they did not then you can file bug in their Github. – Alexander Pavlov Sep 03 '19 at 13:01
  • Alternatively, you can create meta-annotations for every meaningful combination of listeners, i.e. `@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @TestExecutionListeners( listeners = {DbTestListener.class, MessagingTestListener.class}, mergeMode = MERGE_WITH_DEFAULTS) public @interface DatabaseAndMessagingIntegrationTest { }` – Alexander Pavlov Sep 03 '19 at 13:03