Spring beans redefinition in unit test environment

前端 未结 13 1787
醉话见心
醉话见心 2020-12-07 11:13

We are using Spring for my application purposes, and Spring Testing framework for unit tests. We have a small problem though: the application code loads a Spring application

相关标签:
13条回答
  • 2020-12-07 12:03

    One of the reasons spring is described as test-friendly is because it may be easy to just new or mock stuff in the unit test.

    Alternately we have used the following setup with great success, and I think it is quite close to what you want, I would strongly recommend it:

    For all beans that need different implementations in different contexts, switch to annotation based wiring. You can leave the others as-is.

    Implement the following set of annotations

     <context:component-scan base-package="com.foobar">
         <context:include-filter type="annotation" expression="com.foobar.annotations.StubRepository"/>
         <context:include-filter type="annotation" expression="com.foobar.annotations.TestScopedComponent"/>
         <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
     </context:component-scan>
    

    Then you annotate your live implementations with @Repository, your stub implementations with @StubRepository, any code that should be present in the unit-test fixture ONLY with @TestScopedComponent. You may run into needing a couple more annotations, but these are a great start.

    If you have a lot of spring.xml, you will probably need to make a few new spring xml files that basically only contain the component-scan definitions. You'd normally just append these files to your regular @ContextConfiguration list. The reason for this is because you frequently end up with different configurations of the context-scans (trust me, you will make at least 1 more annotations if you're doing web-tests, which makes for 4 relevant combinations)

    Then you basically use the

    @ContextConfiguration(locations = { "classpath:/path/to/root-config.xml" })
    @RunWith(SpringJUnit4ClassRunner.class)
    

    Note that this setup does not allow you to have alternating combinations of stub/live data. We tried this, and I think that resulted in a mess I wouldn't recommend anyone ;) We either wire inn the full set of stubs or the full set of live services.

    We mainly use auto-wired stub dependencies when testing gui near stuff where the dependencies are usually quite substantial. In cleaner areas of the code we use more regular unit-testing.

    In our system we have the following xml-files for component-scan:

    • for regular web production
    • for starting web with stubs only
    • for integration tests (in junit)
    • for unit tests (in junit)
    • for selenium web tests (in junit)

    This means we totally have 5 different system-wide configurations that we can start the application with. Since we only use annotations, spring is fast enough to autowire even those unit tests we want wired. I know this is untraditional, but it's really great.

    Out integration tests run with full live setup, and once or twice I have decided to get really pragmatic and want to have a 5 live wirings and a single mock:

    public class HybridTest {
       @Autowired
       MyTestSubject myTestSubject;
    
    
       @Test
       public void testWith5LiveServicesAndOneMock(){
         MyServiceLive service = myTestSubject.getMyService();
         try {
              MyService mock = EasyMock.create(...)
              myTestSubject.setMyService( mock);
    
               .. do funky test  with lots of live but one mock object
    
         } finally {
              myTestSubject.setMyService( service);
         }
    
    
       }
    }
    

    I know the test purists are going to be all over me for this. But sometimes it's just a very pragmatic solution that turns out to be very elegant when the alternative would be really really ugly. Again it's usually in those gui-near areas.

    0 讨论(0)
  • 2020-12-07 12:04

    spring-reinject is designed to substitute beans with mocks.

    0 讨论(0)
  • 2020-12-07 12:09

    I would propose a custom TestClass and some easy rules for the locations of the spring bean.xml:

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = {
        "classpath*:spring/*.xml",
        "classpath*:spring/persistence/*.xml",
        "classpath*:spring/mock/*.xml"})
    @Transactional
    @TestExecutionListeners({
        DependencyInjectionTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class})
    public abstract class AbstractHibernateTests implements ApplicationContextAware {
    
        /**
         * Logger for Subclasses.
         */
        protected final Logger log = LoggerFactory.getLogger(getClass());
    
        /**
         * The {@link ApplicationContext} that was injected into this test instance
         * via {@link #setApplicationContext(ApplicationContext)}.
         */
        protected ApplicationContext applicationContext;
    
        /**
         * Set the {@link ApplicationContext} to be used by this test instance,
         * provided via {@link ApplicationContextAware} semantics.
         */
        @Override
        public final void setApplicationContext(
                final ApplicationContext applicationContext) {
            this.applicationContext = applicationContext;
        }
    }
    

    If there are mock-bean.xml in the specified location, they will override all "real" bean.xml files in the "normal" locations - your normal locations might differ.

    But … I would never mix mock and non-mock beans, as it's hard to trace problems when the application grows older.

    0 讨论(0)
  • 2020-12-07 12:09

    You do not need to use any test contexts (doesn't matter is XML or Java based). Since Spring boot 1.4 there is available new annotation @MockBean which introduced native support for mocking and Spying of Spring Beans.

    0 讨论(0)
  • 2020-12-07 12:09

    I want to do the same thing, and we're finding it essential.

    The current mechanism we use is fairly manual but it works.

    Say for instance, you wish to mock out bean of type Y. What we do is every bean that has that dependency we make implement an interface - "IHasY". This interface is

    interface IHasY {
       public void setY(Y y);
    }
    

    Then in our test we call the util method...

     public static void insertMock(Y y) {
            Map invokers = BeanFactory.getInstance().getFactory("core").getBeansOfType(IHasY.class);
            for (Iterator iterator = invokers.values().iterator(); iterator.hasNext();) {
                IHasY invoker = (IHasY) iterator.next();
                invoker.setY(y);
            }
        }
    

    I do not want to create a whole xml file just to inject this new dependency and that is why I like this.

    If you're willing to create an xml config file then the way to go would be to create a new factory with the mock beans and make your default factory a parent of this factory. Make sure then that you load all your beans from the new child factory. When doing this the sub-factory will override the beans in the parent factory when the bean id's are the same.

    Now if, in my test, If I could programmatically create a factory, that would be awesome. Having to use xml is just too cumbersome. I'm looking to create that child factory with code. Then each test can configure its factory the way it wants. There's no reason why a factory like that won't work.

    0 讨论(0)
  • 2020-12-07 12:10

    You can also write your unit tests to not require any lookups at all:

    @ContextConfiguration(locations = { "classpath:/path/to/test-config.xml" })
    @RunWith(SpringJUnit4ClassRunner.class)
    public class MyBeanTest {
    
        @Autowired
        private MyBean myBean; // the component under test
    
        @Test
        public void testMyBean() {
            ...
        }
    }
    

    This gives an easy way to mix and match real config files with test config files.

    For example, when using hibernate, I might have my sessionFactory bean in one config file (to be used in both the tests and the main app), and have by dataSource bean in another config file (one might use a DriverManagerDataSource to an in-memory db, the other might use a JNDI-lookup).

    But, definitely take heed of @cletus's warning ;-)

    0 讨论(0)
提交回复
热议问题