Announcement Announcement Module
Collapse
No announcement yet.
How to substitute mock objects into Spring-processed contexts Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • How to substitute mock objects into Spring-processed contexts

    I work in a web/enterprise development company where we use Spring heavily, along with TDD and Mock-based testing. A problem has come up several times that we haven't understood how to solve: how to mix trained mock objects (Easymock) with Spring-wired beans.

    There are times when we want to load a graph of Spring beans, but substitute a mock object (with expectations set on it) for one of the beans, then put a call through and verify the mock was called. Note that we _do_ want to test the Spring wiring & post-processing (which provides a great deal of our apps functionality) so manual wiring is not sufficient.

    A current example is interaction-testing web service beans exposed via XFire/Spring using JSR181 @WebService. We want to test the web services but mock out the backing business logic components.

    A solution would be to bind the trained mock object into the context under a given bean name (somehow?). However, there appears to be no such facility in the spring architecture. This is presumably a deliberate, considered decision, whose motivations I would like to understand.

    In the absence of this, what patterns are recommended for solving this problem?

    -Ben

  • #2
    +1

    I also want to do this. This would seem to be a common requirement: a full integration test by mocking out just a couple beans. I'm going to investigate subclassing ClassPathXmlApplicationContext to try to accomplish this.

    Comment


    • #3
      Yes, it _is_ a common requirement, and Im disappointed by the silence of the SpringSource team on this issue (now and in previous similar posts). For me, as a convinced "mockist" tester, its a day-to-day obstacles of working with Spring.

      I dont think its as easy as customizing the App Context: Springs cache of actual instantiated bean objects is not exposed. Spring does offer extension points to modify the bean definition (the "recipe" for a bean) but thats insufficient for mocks because one needs to inject a particular trained instance.

      I got a good reply from Robert Blumen on the springframework user list, which I include at the end of this message, but the complexity of his solution simply reinforces that Spring poorly caters for this need by default.

      Another (possibly simpler) solution involves using Spring's new Java Config mechanism, where beans are configured using Java code, which would allow a mock to be trained. I will investigate.

      ===================================
      Robert Blumen wrote:
      > We have something like this working here.
      >
      > The outline of how it works is:
      >
      > 1. We have a custom auto-proxy creator that uses the
      > Spring HotswappableTargetSource as the target source
      > for those beans we plan to replace.
      >
      > Only certain beans (usually a handful) are auto-proxied.
      > This avoids having the entire container, containing
      > hundreds of beans auto-proxied.
      >
      > 2. There are two ways that the auto-proxy creator determines
      > which beans to set up with a HS target source:
      >
      > Either
      > a. The test base class communicate with
      > the auto-proxy creator which beans to auto-proxy,
      > based on annotations of dependency-injected beans
      > within the test class.
      >
      > or
      > b. The auto-proxy creator is configured for each test
      > class with a list of beans to auto-proxy.
      >
      > 3. We initially load the application context files with all the
      > spring-wired beans (which includes certain beans that have
      > been proxied with HSTS) . Then, each test case creates the
      > mocks that it wants to use instead and swaps those
      > mocks in place of the wired beans. Mocks can only be
      > created for those beans that have been proxied with HSTS.
      >
      > -> this approach allows us to make all of the changes within
      > the test itself, while the default application context remains
      > unchanged.
      >
      > 4. A test case that wants to swap in place a mock creates
      > the mock, then calls a hotSwap(mock) method. The implementation
      > of this method does the swap as follows:
      >
      > a. obtain the bean from the app context
      > b. cast to Advised to get the proxy
      > c. get the HS target source from the proxy
      > d. calls swap() on the target source
      > e. saves the original target adding it to the undo list
      >
      > 5. The tearDown method does an undo() operation, swapping
      > the wired beans back in, so each test case can start from
      > a fresh app context with all of the originally wired beans.

      Comment


      • #4
        Possible solution

        I came up with this, which seems to work for the couple of cases I've tested. Only tested with Spring 2.5.

        Code:
        package ca.digitalrapids.spring.context.support;
        
        import java.util.HashMap;
        import java.util.Map;
        
        import org.springframework.beans.BeansException;
        import org.springframework.beans.factory.BeanFactory;
        import org.springframework.beans.factory.support.DefaultListableBeanFactory;
        import org.springframework.context.ApplicationContext;
        import org.springframework.context.support.ClassPathXmlApplicationContext;
        
        /**
         * Subclass of {@link ClassPathXmlApplicationContext} that allows overriding of
         * individual beans with custom instantiations.  This is useful mocking out
         * particular beans in integration tests.  This does NOT work with autowiring.
         */
        @SuppressWarnings("unchecked")
        public class OverridableClassPathXmlApplicationContext 
            extends ClassPathXmlApplicationContext
        {
            private class MyDefaultListableBeanFactory extends DefaultListableBeanFactory {
                public MyDefaultListableBeanFactory(BeanFactory parentBeanFactory)
                {
                    super(parentBeanFactory);
                }
        
                @Override
                public Object getBean(final String name, final Class requiredType, 
                    final Object[] args) throws BeansException {
                    Object bean = singletonsByBeanName.get(name);
                    return bean == null ? super.getBean(name, requiredType, args) : bean;
                }
            }
            private final Map<String, Object> singletonsByBeanName = new HashMap<String, Object>();
            
            public OverridableClassPathXmlApplicationContext(String[] configLocations, 
                boolean refresh,
                ApplicationContext parent,
                Map<String, Object> singletonsByBeanName) throws BeansException
            {
                super(configLocations, false, parent);
                if ( singletonsByBeanName != null )
                    this.singletonsByBeanName.putAll(singletonsByBeanName);
                if ( refresh ) refresh();
            }
        
            @Override
            protected DefaultListableBeanFactory createBeanFactory()
            {
                return new MyDefaultListableBeanFactory(getInternalParentBeanFactory());
            }
        
        }
        Unit test:

        Code:
        package ca.digitalrapids.spring.context.support;
        
        import java.util.HashMap;
        import java.util.Map;
        
        import org.junit.Before;
        import org.junit.Test;
        
        import static org.junit.Assert.*;
        
        
        public class OverridableClassPathXmlApplicationContextTest
        {
            private static final String TO_BE_OVERRIDEN_BEAN_NAME = "toBeOverriden";
            private static final String ANOTHER_BEAN_NAME = "anotherBean";
        
            public interface SomeInterface {
            }
            static public class SomeInterfaceImpl implements SomeInterface {
            }
            static public class SomeInterfaceImpl2 implements SomeInterface {
            }
            static public class AnotherBean {
                private final SomeInterface dependency;
                private SomeInterface dependency2;
                public AnotherBean(SomeInterface dependency)
                {
                    super();
                    this.dependency = dependency;
                }
                public SomeInterface getDependency()
                {
                    return dependency;
                }
                public SomeInterface getDependency2()
                {
                    return dependency2;
                }
                public void setDependency2(SomeInterface dependency2)
                {
                    this.dependency2 = dependency2;
                }
            }
            private OverridableClassPathXmlApplicationContext context;
            
            @Before
            public void setUp() throws Exception
            {
            }
        
            @Test
            public void testNoOverride() throws Throwable
            {
                context = new OverridableClassPathXmlApplicationContext(new String[] {
                    "ca/digitalrapids/spring/context/support/spring-test-beans.xml"
                    }, true, null, null);
                assertEquals(SomeInterfaceImpl.class, 
                    context.getBean(TO_BE_OVERRIDEN_BEAN_NAME).getClass());
                AnotherBean anotherBean = (AnotherBean)context.getBean(ANOTHER_BEAN_NAME);
                assertNotNull(anotherBean);
                assertEquals(SomeInterfaceImpl.class, anotherBean.getDependency().getClass());
                assertEquals(SomeInterfaceImpl.class, anotherBean.getDependency2().getClass());
            }
            
            @Test
            public void testOverride() throws Throwable
            {
                SomeInterface overrider = new SomeInterfaceImpl2();
                Map<String, Object> singletonsByBeanName = new HashMap<String, Object>();
                singletonsByBeanName.put(TO_BE_OVERRIDEN_BEAN_NAME, overrider);
                context = new OverridableClassPathXmlApplicationContext(new String[] {
                    "ca/digitalrapids/spring/context/support/spring-test-beans.xml"
                }, true, null, singletonsByBeanName);
                
                assertEquals(SomeInterfaceImpl2.class, 
                    context.getBean(TO_BE_OVERRIDEN_BEAN_NAME).getClass());
        
                AnotherBean anotherBean = (AnotherBean)context.getBean(ANOTHER_BEAN_NAME);
                assertNotNull(anotherBean);
                assertEquals(SomeInterfaceImpl2.class, anotherBean.getDependency().getClass());
                assertEquals(SomeInterfaceImpl2.class, anotherBean.getDependency2().getClass());
            }
        }
        spring-test-beans.xml used by unit test:

        Code:
        <?xml version="1.0" encoding="UTF-8"?>
        <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
        "http://www.springframework.org/dtd/spring-beans.dtd">
        
        <beans>
            
            <bean id="toBeOverriden" 
                class="ca.digitalrapids.spring.context.support.OverridableClassPathXmlApplicationContextTest$SomeInterfaceImpl"/>
            
            <bean id="anotherBean" 
                class="ca.digitalrapids.spring.context.support.OverridableClassPathXmlApplicationContextTest$AnotherBean">
                <constructor-arg ref="toBeOverriden"/>
                <property name="dependency2" ref="toBeOverriden"/>
            </bean>
        
        </beans>

        Comment


        • #5
          Hi,

          I work on Spring.NET at SpringSource and ran across this thread because I needed to do something similar on .NET. You can take a look here on the .NET forums for the solution I came up with, it does work with autowiring. I also integrated a little bit with automatic DI of protected fields that have a special annotation.

          The container internals are not 1-1 between .NET and Java. In the .NET case, I overrode the method FindMatchingObjects called in AbstractAutowireCapableObjectFactory's method AutowireByType. In the Java version AbstractAutowireCapableBeanFactory has a similar method but delegates to the method resolveDependency(). You should override that method and then provide an implementation similar to what I used. To get the ApplicationContext semantics, you might consider using GenericApplicationContext and passing in your new subclassed bean factory as the underlying implementation. That is what I did in the base class AbstractMockContainerTests. I'm sure this could be ported over to JUnit4 style.

          Hope this helps. If you run into trouble, let me know and I'll take a shot at the Java version. I'm interested in your feedback as well!

          Cheers,
          Mark

          Comment


          • #6
            Originally posted by ben_hutchison View Post
            There are times when we want to load a graph of Spring beans, but substitute a mock object (with expectations set on it) for one of the beans, then put a call through and verify the mock was called. Note that we _do_ want to test the Spring wiring & post-processing (which provides a great deal of our apps functionality) so manual wiring is not sufficient.
            Why don't you just load up your Spring beans as normal, then replace the dependencies you want to mock by creating a Mock object and calling the setter of the dependency ?

            Very easy, and very flexible.

            Comment


            • #7
              Hi,

              I guess a reason against that approach would be that you are introducing the setter just for the purposes of testing, for example if you had set all your dependencies via contructor wiring and wanted those implementations to be immutable from the typical client's view of the API.

              Mark

              Comment


              • #8
                Sorry for the year-long delay.

                I'm not really concerned about autowiring, and wouldn't moving to GenericApplicationContext leave the behind the functionality of ClassPathXmlApplicationContext, which I need?

                Sorry, I can't seem to muster the will to enhance this class, as it suits my needs as is. I was hoping the Spring team would take the baton and add something equivalent to the Spring test library in a future release. I didn't see anything in Spring 2.5, though.

                Comment

                Working...
                X