5

I was asked to create integration tests for an existing SpringBoot project, whose organisation is not as modular as I would prefer. For example, there is a package yielding all repositories associated with all services. This became a problem for me when I was attempting to create a @WebMvcTest test slice, because when I use @ComponentScan, @EnableJpaRepositories, @EntityScan to read my target classes it ends up scanning all other unnecessary ones that share the same package.

Since changing the project structure is not really a decision I can make on my own, my question is whether it is possible to have my test scan pick a specific class and disregard all others within the same package?

Thank you for your attention

João Matos
  • 6,102
  • 5
  • 41
  • 76

2 Answers2

2

I was finally able to achieve all the required filtering, thanks to Josef's answer and these:

Components and Services can be configured to yield filters hence we can specify our target services and controllers and exclude everything else at the same time:

 @ComponentScan(
        basePackageClasses = {
                MyTargetService.class,
                MyTargetController.class
        },
        useDefaultFilters = false,
        includeFilters = {
                @ComponentScan.Filter(type = ASSIGNABLE_TYPE, value = MyTargetService.class),
                @ComponentScan.Filter(type = ASSIGNABLE_TYPE, value = MyTargetController.class)

        }
)

Repositories. This is unlikely to work for repositories, but fortunately the @EnableJpaRepositories supports the same type of filters:

  @EnableJpaRepositories(
       basePackageClasses = {
            MyTargetRepository.class
       },
       includeFilters = {
            @ComponentScan.Filter(type = ASSIGNABLE_TYPE, value = MyTargetRepository.class)
       }
  )

Entities. This part is more tricky because @EntityScan does not support these filters. Although the entities do not reference Spring beans, I prefer loading only the entities necessary for my test. I was not able to find any annotation for entities that supports filtering, but we can filter them programmatically using a PersistenceUnitPostProcessor in our EntityManagerFactory. Here is my full solution:

   //add also the filtered @ComponentScan and @EnableJpaRepositories annotations here
   @Configuration
   public class MyConfig {

    //here we specify the packages of our target entities
    private static String[] MODEL_PACKAGES = {
            "com.full.path.to.entity.package1",
            "com.full.path.to.entity.package2"
    };

    //here we specify our target entities
    private static Set<String> TARGET_ENTITIES = new HashSet<>(Arrays.asList(
            "com.full.path.to.entity.package1.MyTargetEntity1",
            "com.full.path.to.entity.package2.MyTargetEntity2"
    ));

    @Bean
    public DataSource getDataSource() {
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        return builder.setType(EmbeddedDatabaseType.H2).build();
    }

    @Bean
    public EntityManagerFactory entityManagerFactory() {

        ReflectionsPersistenceUnitPostProcessor reflectionsPersistenceUnitPostProcessor = new ReflectionsPersistenceUnitPostProcessor();

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setGenerateDdl(true);
        vendorAdapter.setShowSql(true);

        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(vendorAdapter);
        factory.setPackagesToScan(MODEL_PACKAGES);
        factory.setDataSource(getDataSource());
        factory.setPersistenceUnitPostProcessors(reflectionsPersistenceUnitPostProcessor);
        factory.afterPropertiesSet();

        return factory.getObject();
    }


    @Bean
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory());
        return txManager;
    }

    public class ReflectionsPersistenceUnitPostProcessor implements PersistenceUnitPostProcessor {

        @Override
        public void postProcessPersistenceUnitInfo(MutablePersistenceUnitInfo pui) {

            Reflections r = new Reflections("", new TypeAnnotationsScanner());
            Set<Class<?>> entityClasses = r.getTypesAnnotatedWith(Entity.class, true);
            Set<Class<?>> mappedSuperClasses = r.getTypesAnnotatedWith(MappedSuperclass.class, true);

            pui.getManagedClassNames().clear(); //here we remove all entities

            //here we add only the ones we are targeting
            for (Class<?> clzz : mappedSuperClasses) {
                if (TARGET_ENTITIES.contains(clzz.getName())) {
                    pui.addManagedClassName(clzz.getName());
                }
            }
            for (Class<?> clzz : entityClasses) {
                if (TARGET_ENTITIES.contains(clzz.getName())) {
                    pui.addManagedClassName(clzz.getName());
                }
            }

        }

    }


}
João Matos
  • 6,102
  • 5
  • 41
  • 76
1

ComponentScan can work with slices. - It's actually configured on SpringBootApplication annotation itself. The part that makes test slices work with ComponentScan is TypeExcludeFilter:

@ComponentScan(
    basePackages = "com.mycompany.someotherpackage",
    excludeFilters = {
      @ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
    })
Josef Cech
  • 2,115
  • 16
  • 17
  • Thanks! How about with entities? If I have a package with numerous entities but I am interested in only one of them? – João Matos Feb 17 '20 at 13:49
  • @JoãoMatos Unfortunately I don't have any experience with that. Usually entities are referencing each other so more of them must be loaded anyway. But at the same time - there's no expense in loading them all apart from time spent on generating tables in DB etc. They are not referencing Spring beans so future changes should not break your existing tests. – Josef Cech Feb 17 '20 at 17:53