How to mock absent bean definitions in SpringJUnit4ClassRunner?
Asked Answered
C

4

5

I have a Spring 4 JUnit test which should verify only a particular part of my application.

@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:context-test.xml")
@ActiveProfiles("test")
public class FooControllerIntegrationTest {
     ...
}

So I don't want to configure and instantiate all those beans which are actually aren't involved into the scope of my test. For example I don't want to configure beans which are used in another controller which I am not going to test here.

However, because I don't want to narrow component-scan pathes, I get "No qualifying bean of type" exception:

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [...

Is any way how to ignore such missed definitions if I certainly sure that they aren't involved into the functionality I am testing?

Crisis answered 6/3, 2018 at 6:47 Comment(0)
C
2

I have found a way how to automatically mock absent bean definitions.

The core idea is to create own BeanFactory:

public class AutoMockBeanFactory extends DefaultListableBeanFactory {

    @Override
    protected Map<String, Object> findAutowireCandidates(final String beanName, final Class<?> requiredType, final DependencyDescriptor descriptor) {
        String mockBeanName = Introspector.decapitalize(requiredType.getSimpleName()) + "Mock";
        Map<String, Object> autowireCandidates = new HashMap<>();
        try {
            autowireCandidates = super.findAutowireCandidates(beanName, requiredType, descriptor);
        } catch (UnsatisfiedDependencyException e) {
            if (e.getCause() != null && e.getCause().getCause() instanceof NoSuchBeanDefinitionException) {
                mockBeanName = ((NoSuchBeanDefinitionException) e.getCause().getCause()).getBeanName();
            } 
            this.registerBeanDefinition(mockBeanName, BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition());
        }
        if (autowireCandidates.isEmpty()) {
            final Object mock = mock(requiredType);
            autowireCandidates.put(mockBeanName, mock);
            this.addSingleton(mockBeanName, mock);
        }
        return autowireCandidates;
    }
}

It also should be registered by creating own AbstractContextLoader implementation, based on the GenericXmlWebContextLoader. Unfortunately the latter one has a final loadContext(MergedContextConfiguration mergedConfig) method, so it is needed to fully copy its implementation (say into class AutoMockGenericXmlWebContextLoader) with one difference:

GenericWebApplicationContext context = new GenericWebApplicationContext(new AutoMockBeanFactory());

No it can be used in the test:

@ContextConfiguration(
     value = "classpath:context-test.xml", 
     loader = AutoMockGenericXmlWebContextLoader.class)
Crisis answered 11/3, 2018 at 12:52 Comment(1)
That's a very interesting, custom solution. I haven't analyzed the implementation thoroughly; however, if you are only interested in mocking missing beans injected via autowiring (and if it indeed works for you), then by all means... use it!Centigram
C
4

Is any way how to ignore such missed definitions if I certainly sure that they aren't involved into the functionality I am testing?

No, there is no automated or built-in mechanism for such a purpose.

If you are instructing Spring to load beans that have mandatory dependencies on other beans, those other beans must exist.

For testing purposes, the best practices for limiting the scope of which beans are active include modularization of your config (e.g., horizontal slicing that allows you to selectively choose which layers of your application are loaded) and the use of bean definition profiles.

If you're using Spring Boot, you can then also make use of "testing slices" or @MockBean/@SpyBean in Spring Boot Test.

However, you should keep in mind that it's typically not a bad thing to load beans that you are not using in a given integration test, since you are (hopefully) testing other components that in fact need those beans in other test classes within your test suite, and the ApplicationContext would then be loaded only once and cached across your different integration testing classes.

Regards,

Sam (author of the Spring TestContext Framework)

Centigram answered 7/3, 2018 at 15:7 Comment(2)
I appreciate your answer! Thank you a lot. But I am still considering writing my own test tool which automatically mock missed dependencies.Crisis
I have at least found a workable solution. Please, check my self-answer: https://mcmap.net/q/2027719/-how-to-mock-absent-bean-definitions-in-springjunit4classrunner . I am really interested in your opinion.Crisis
C
2

I have found a way how to automatically mock absent bean definitions.

The core idea is to create own BeanFactory:

public class AutoMockBeanFactory extends DefaultListableBeanFactory {

    @Override
    protected Map<String, Object> findAutowireCandidates(final String beanName, final Class<?> requiredType, final DependencyDescriptor descriptor) {
        String mockBeanName = Introspector.decapitalize(requiredType.getSimpleName()) + "Mock";
        Map<String, Object> autowireCandidates = new HashMap<>();
        try {
            autowireCandidates = super.findAutowireCandidates(beanName, requiredType, descriptor);
        } catch (UnsatisfiedDependencyException e) {
            if (e.getCause() != null && e.getCause().getCause() instanceof NoSuchBeanDefinitionException) {
                mockBeanName = ((NoSuchBeanDefinitionException) e.getCause().getCause()).getBeanName();
            } 
            this.registerBeanDefinition(mockBeanName, BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition());
        }
        if (autowireCandidates.isEmpty()) {
            final Object mock = mock(requiredType);
            autowireCandidates.put(mockBeanName, mock);
            this.addSingleton(mockBeanName, mock);
        }
        return autowireCandidates;
    }
}

It also should be registered by creating own AbstractContextLoader implementation, based on the GenericXmlWebContextLoader. Unfortunately the latter one has a final loadContext(MergedContextConfiguration mergedConfig) method, so it is needed to fully copy its implementation (say into class AutoMockGenericXmlWebContextLoader) with one difference:

GenericWebApplicationContext context = new GenericWebApplicationContext(new AutoMockBeanFactory());

No it can be used in the test:

@ContextConfiguration(
     value = "classpath:context-test.xml", 
     loader = AutoMockGenericXmlWebContextLoader.class)
Crisis answered 11/3, 2018 at 12:52 Comment(1)
That's a very interesting, custom solution. I haven't analyzed the implementation thoroughly; however, if you are only interested in mocking missing beans injected via autowiring (and if it indeed works for you), then by all means... use it!Centigram
E
0

If you don't narrow your component-scan, then usually you would have all the beans available to the test, EXCEPT some specific ones that become available conditionally (e.g. beans defined by spring-batch)

In this case, one option that has worked for me is to mark such dependencies and components as @Lazy. This will make sure that they will only be loaded when needed. Note that (depending on scenario) you may have to mark both the @Autowired dependency and the @Component as @Lazy

Eri answered 6/3, 2018 at 6:56 Comment(4)
But I don't want to mark my beans as Lazy in the production code. This will break the precheck when application is loading.Crisis
Sure... Although, if you do have tests against components that use the lazy parts, then that risk is covered...Eri
Anyway, thanks for your answer. But I am pretty sure there is some elegant way. For instance some custom bean loader which automatically mocks missed dependencies with Mockito.Crisis
Sure, no problemEri
S
0

Like the OP posted, here is the annotation context equivalent to inject any mock missing beans:

context = new CustomAnnotationConfigApplicationContext(SpringDataJpaConfig.class);

public class CustomAnnotationConfigApplicationContext extends AnnotationConfigApplicationContext {

    public CustomAnnotationConfigApplicationContext() {
        super(new AutoMockBeanFactory());
    }

    public CustomAnnotationConfigApplicationContext(Class<?>... annotatedClasses) {
        this();
        this.register(annotatedClasses);
        this.refresh();
    }
}


public class AutoMockBeanFactory extends DefaultListableBeanFactory {

    @Override
    protected Map<String, Object> findAutowireCandidates(final String beanName, final Class<?> requiredType, final DependencyDescriptor descriptor) {
        String mockBeanName = Introspector.decapitalize(requiredType.getSimpleName());
        Map<String, Object> autowireCandidates = new HashMap<>();
        try {
            autowireCandidates = super.findAutowireCandidates(beanName, requiredType, descriptor);
        } catch (UnsatisfiedDependencyException e) {
            if (e.getCause() != null && e.getCause().getCause() instanceof NoSuchBeanDefinitionException) {
                mockBeanName = ((NoSuchBeanDefinitionException) e.getCause().getCause()).getBeanName();
            }
            this.registerBeanDefinition(mockBeanName, BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition());
        }
        if (autowireCandidates.isEmpty()) {
            System.out.println("Mocking bean: " + mockBeanName);
            final Object mock = Mockito.mock(requiredType);
            autowireCandidates.put(mockBeanName, mock);
            this.addSingleton(mockBeanName, mock);
        }
        return autowireCandidates;
    }
}
Spike answered 8/4, 2020 at 17:28 Comment(2)
Would you care to give an example as to how this should be used ? I'm trying to use this in my test environment with @DataJpaTest and @ExtendWith(SpringExtension.class)Crossover
@Crystark, this is the full example by itself, which type (JPA) or pattern (*Repo) of beans you want to mock is up to you. This is useful in integration tests in some projects where some beans cannot be created or not needed.Spike

© 2022 - 2024 — McMap. All rights reserved.