Reputation: 7020
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
Upvotes: 6
Views: 1915
Reputation: 472
Based on João Matos answer and a response to a github issue I was able to put together a shorter complete solution. The difference here is that all of the hibernate setup is left to Spring Boot. Following sample limits the repositories to MyEntityRepository and entities to MyEntity.
@RunWith(SpringRunner.class)
@DataJpaTest
@ContextConfiguration(classes = MyEntityRepositoryTest.TestConfiguration.class)
public class MyRepositoryTest {
@Configuration
@EnableJpaRepositories(basePackageClasses = MyEntityRepository.class,
includeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = MyEntityRepository.class) })
static class TestConfiguration {
public class CustomPersistenceUnitPostProcessor implements PersistenceUnitPostProcessor {
@Override
public void postProcessPersistenceUnitInfo(MutablePersistenceUnitInfo pui) {
pui.addManagedClassName(MyEntity.class.getName());
}
}
@Component
class CustomBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof LocalContainerEntityManagerFactoryBean) {
((LocalContainerEntityManagerFactoryBean) bean)
.setPersistenceUnitPostProcessors(new CustomPersistenceUnitPostProcessor());
}
return bean;
}
}
}
@Autowired
MyEntityRepository myEntityRepository;
@Test
public void test() {
...
}
}
Upvotes: 0
Reputation: 7020
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());
}
}
}
}
}
Upvotes: 3
Reputation: 2275
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),
})
Upvotes: 1