2

I have this JUnit 5 test:

@SpringBootTest
public class MyTest {
   ...
}

The application default configuration loads many @Component's @Service's and @Configuration's.

For some tests, I would like to tell the Spring application running in this JUnit test context to not scan all the components and filter out some heavy loading or verbose/spammy components that might complain while running in a (mocked) environment where not all the requirements are met.

I tried to provide the @ComponentScan annotation to MyTest class in the form below. I added on purpose multiple filters and multiple types being filtered with the hope that I see less beans being loaded/registered in the application context:

@ComponentScan(
    useDefaultFilters = false,
    excludeFilters = {
        @Filter(type = FilterType.ANNOTATION, classes = {
            org.springframework.context.annotation.Configuration.class,
            org.springframework.stereotype.Service.class,
            org.springframework.stereotype.Component.class
        }),
        @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {
            MyRabbitMqListener.class
        })
    }
)
@SpringBootTest
public class MyTest {

    @Autowired
    private ApplicationContext context;

    @Test
    public void expectingLessRegisteredBeans() {
        List<String> beans = Arrays.stream(context.getBeanDefinitionNames())
               // ...
               .collect(Collectors.toList());
        assertEquals(beans.size(), ...);
    }

}

Regardless what I provide to the @CompoenntScan, I don't manage to control the amount or what beans are being scanned/registered in the JUnit 5 Spring test context.

But I still want to use the @SpringBootTest in order to get most of my application's configuration but exclude some parts of it (because they are slow, or spammy in the logs, or just throwing errors). For example, an application that receives event from inputs like Kafka, RabbitMQ, or REST, processes them and saves them to the persistence layer. I want to test the processing and persistence integration. I throw a bunch of test events to the processor @Service and then expect to see the results in the DB via the @Repository.

What would be my alternatives:

  • provide @SpringBootTest a whitelist of classes I want to load (not elegant, tedious)
  • define several Spring profiles, put condition annotations on component and activate/deactivate only the ones I need in the test code, also use different profile application-*.properties to mute some logging (the non-test code needs to be polluted with this test feature, and tedious creating multiple profiles)
  • other ways to build the application context from scratch (while I actually want is to use my application configuration, except some slices which are not relevant for certain tests)
Gabriel Petrovay
  • 20,476
  • 22
  • 97
  • 168
  • 1
    Then don't use `@SpringBootTest` as that is intended for a full integration test. If you want something else either use a slices test (`@WebMvcTest` for instance), write a proper unit test (using mocks) or use a regular Spring test with a dedicated Test context. – M. Deinum Mar 17 '21 at 13:49
  • I want to write a test that is concerned with everything but the Kafka, Rabbit, Rest inputs. I am still trying still to write integration tests that get different messages and throw them to the Services/Processors and then I want to see the expected results in the Oracle or the H2 DB. Some Repository queries require Oracle built-in JSON_QUERY querying feature, so I have to do integration tests for these features. – Gabriel Petrovay Mar 17 '21 at 13:54
  • Remove the `@ComponentScan` stuff and instead define what classes should be managed by Spring via the `classes` attribute on the `@SpringBootTest` annotation, i.e. like this: `@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = { Config1.class, Config2.class, SomeAutoConfiguration.class, Component1.class, Service2.class})`. Some further information can be found [in this answer](https://stackoverflow.com/questions/42275732/spring-boot-apache-camel-routes-testing/51892629#51892629) – Roman Vottner Mar 17 '21 at 14:34
  • Having only NONE environment and 2 classes (the service in test and the repository behind it). The application fails to load: `expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}`. The error complains about not finding the repository class and hence not being able to autowire the service in the test class. But, as mentioned, both the service and the repository are provided in the classes annotation parameter. :( – Gabriel Petrovay Mar 17 '21 at 15:26

1 Answers1

0

You can use a org.springframework.boot.test.context.filter.TypeExcludeFilter.

On your @SpringBootTest annotated integration test add a @TypeExcludeFilters(YourCustomTypeExcludeFilter.class) annotation and it will use your implemented TypeExcludeFilter to filter out beans you don't want from the test's ApplicationContext.


@SpringBootTest
@TypeExcludeFilters(YourCustomTypeExcludeFilter.class)
public class MyTest {

    @Autowired
    private ApplicationContext context;

    @Test
    public void expectingLessRegisteredBeans() {
        List<String> beans = Arrays.stream(context.getBeanDefinitionNames())
               // ...
               .collect(Collectors.toList());
        assertEquals(beans.size(), ...);
    }

}

A simple TypeExcludeFilter that excludes beans by class name is the one below:

public class YourCustomTypeExcludeFilter extends TypeExcludeFilter {

  private static final List<String> beanNamesToExclude = List.of(
      "org.personal.MyRabbitMqListener");

  @Override
  public boolean equals(Object obj) {
    return (obj != null) && (getClass() == obj.getClass());
  }

  @Override
  public int hashCode() {
    return getClass().hashCode();
  }

  @Override
  public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) {
    return beanNamesToExclude.stream()
        .anyMatch(beanName -> metadataReader.getClassMetadata().getClassName().equals(beanName));
  }
}

Sky's the limit on the logic you can use with the match() method. For some more inspiration of what is possible just search for classes that extend TypeExcludeFilter in the Spring Boot source code (for instance look at org.springframework.boot.test.context.filter.TestTypeExcludeFilter, a filter that's used to exclude @TestConfiguration annotated classes)

jucosorin
  • 1
  • 1