Статьи

Как использовать Mock / Stub в Spring Integration Tests

Как правило, вы выбираете подмножество компонентов в некоторых интеграционных тестах, чтобы проверить, склеены ли они должным образом. Чтобы достичь этого, их обычно действительно вызывают, но иногда это слишком дорого. Например, компонент A вызывает компонент B, а компонент B зависит от внешней системы, у которой нет тестового сервера. Мы действительно хотим проверить конфигурации, кажется, единственный способ — заменить Компонент B на двойной тест после подключения Компонентов A и B.

Давайте начнем со Стратегии A: Ручной впрыск

@RunWith(SpringJUnit4ClassRunner.class)  
@ContextConfiguration(locations = "classpath:config.xml")  
public class SomeAppIntegrationTestsUsingManualReplacing {  
  
    private Mockery context = new JUnit4Mockery();     (1)  
  
    private SomeInterface mock = context.mock(SomeInterface.class);   (2)  
  
    @Resource(name = "someApp")  
    private SomeApp someApp;                  (3)  
  
    @Before  
    public void replaceDependenceWithMock() {  
        someApp.setDependence(mock);          (4)  
    }  
  
    @DirtiesContext 
    @Test  
    public void returnsHelloWorldIfDependenceIsAvailable() throws Exception {  
  
        context.checking(new Expectations() {  
            {  
                allowing(mock).isAvailable();          
                will(returnValue(true));         (5)  
            }  
        });  
  
        String actual = someApp.returnHelloWorld();  
        assertEquals("helloWorld", actual);  
        context.assertIsSatisfied(); (6)  
    }  
} 


Мы получаем подпружиненный компонент someApp (в данном случае это компонент A), и он имеет зависимость от SomeInterface (в данном случае это компонент B). Мы вводим mock (объявляем и инициируем на шаге 4) в someApp, таким образом, тест проходит без отправки запроса во внешнюю систему. Context.assertIsSatisfied () (на шаге 6) очень важен, так как мы используем SpringJUnit4ClassRunner в качестве бегуна junit вместо JMock, поэтому вы должны явно заявить, что все ожидания удовлетворены.

 Есть две недостатки предыдущей стратегии:

Во-первых, если существует более одного макета, вы должны вводить их один за другим, что очень утомительно, особенно когда вам нужно вводить макеты в несколько весенних бобов.

Во-вторых, проводка не проверена. Например, если я забуду написать <property name = «beanName» ref = «bean» />, интеграционные тесты, использующие стратегию ручного внедрения, не скажут.

Стратегия B: использование предопределенного  BeanPostProcessor

Spring предоставляет BeanPostProcessor, который очень полезен, когда вы хотите заменить какой-либо bean-компонент после завершения подключения. Согласно этой ссылке, контекст приложения будет автоматически определять все BeanPostProcessor, зарегистрированные в метаданных (обычно в формате xml). 

public class PredefinedBeanPostProcessor implements BeanPostProcessor {  
  
    public Mockery context = new JUnit4Mockery();    (1)  
  
    public SomeInterface mock = context.mock(SomeInterface.class);   (2)  
  
    @Override  
    public Object postProcessBeforeInitialization(Object bean, String beanName)  
            throws BeansException {  
        return bean;  
    }  
  
    @Override  
    public Object postProcessAfterInitialization(Object bean, String beanName)  
            throws BeansException {  
        if ("dependence".equals(beanName)) {  
            return mock;  
        } else {  
            return bean;  
        }  
    }  
}  

@RunWith(SpringJUnit4ClassRunner.class)  
@ContextConfiguration(locations = { "classpath:config.xml",  
        "classpath:predefined.xml" })   (1)  
public class SomeAppIntegrationTestsUsingPredefinedReplacing {  
  
    @Resource(name = "someApp")  
    private SomeApp someApp;  
  
    @Resource(name = "predefined")  
    private PredefinedBeanPostProcessor fixture;  
  
    @Test  
    public void returnsHelloWorldIfDependenceIsAvailable() throws Exception {  
  
        fixture.context.checking(new Expectations() {  
            {  
                allowing(fixture.mock).isAvailable();  
                will(returnValue(true));  
            }  
        });  
  
        String actual = someApp.returnHelloWorld();  
        assertEquals("helloWorld", actual);  
        fixture.context.assertIsSatisfied();  
    }  
}  

Notice there is an extra config xml in which the PredefinedBeanPostProcessor is registered(at step 1).  The predefined.xml is placed in src/test/resources/, so it will not be packed into the artifact for production.

 For each test, using Strategy B requires inputting both a java file and a xml which is quite verbose.

 Now we have learned the pros and cons of  Strategy A and Strategy B.  What about a hybrid version — killing two birds with one stone.  Therefore we have the next strategy.

Strategy CDynamic Injecting 

public class TestDoubleInjector implements BeanPostProcessor {  
  
    private static Map<String, Object> MOCKS = new HashMap<String, Object>(); (1)  
  
    @Override  
    public Object postProcessBeforeInitialization(Object bean, String beanName)  
            throws BeansException {  
        return bean;  
    }  
  
    @Override  
    public Object postProcessAfterInitialization(Object bean, String beanName)  
            throws BeansException {  
        if (MOCKS.containsKey(beanName)) {  
            return MOCKS.get(beanName);  
        }  
        return bean;  
    }  
  
    public void addMock(String beanName, Object mock) {  
        MOCKS.put(beanName, mock);  
    }  
  
    public void clear() {  
        MOCKS.clear();  
    }  
  
}  

@RunWith(JMock.class)  
public class SomeAppIntegrationTestsUsingDynamicReplacing {  
  
    private Mockery context = new JUnit4Mockery();  
  
    private SomeInterface mock = context.mock(SomeInterface.class);  
  
    private SomeApp someApp;  
  
    private ConfigurableApplicationContext applicationContext;  
  
    private TestDoubleInjector fixture = new TestDoubleInjector(); (1)  
  
    @Before  
    public void replaceDependenceWithMock() {  
  
        fixture.addMock("dependence", mock);  (2)  
  
        applicationContext = new ClassPathXmlApplicationContext(new String[] {  
                "classpath:config.xml", "classpath:dynamic.xml" });  (3)  
        someApp = (SomeApp) applicationContext.getBean("someApp");  
    }  
  
    @Test  
    public void returnsHelloWorldIfDependenceIsAvailable() throws Exception {  
  
        context.checking(new Expectations() {  
            {  
                allowing(mock).isAvailable();  
                will(returnValue(true));  
            }  
        });  
  
        String actual = someApp.returnHelloWorld();  
        assertEquals("helloWorld", actual);  
    }  
  
    @After  
    public void clean() {  
        applicationContext.close();  
        fixture.clear();  
    }  
}  

The TestDoubleInjector class is an implementation of Monostate pattern. Mocks are added to the static map before the application context being created. When another TestDoubleInjector instance (defined in dynamic.xml) is initiated, it can share the static map for replacement.  Just beware to clear the static map after tests. 

By the way, you could use Stub instead of Mocks with same strategies. 

Please do not hesitate to contact me if you might have any questions.  And I do appreciate it, if you could let me know you have a better idea. Thanks!  

Resources:

http://www.jmock.org

http://www.oracle.com/technetwork/articles/entarch/spring-aop-with-ejb5-093994.html(I saw BeanPostProcessor the first time in this post)