Если нельзя, но очень хочется, то нужно обязательно и ничего в мире не стоит того, чтобы делать из этого проблему!

Если тебе полезно что-то из того, чем я делюсь в своем блоге - можешь поделиться своими деньгами со мной.
с пожеланием
столько времени читатели провели на блоге - 
сейчас онлайн - 
Показаны сообщения с ярлыком Mockito. Показать все сообщения
Показаны сообщения с ярлыком Mockito. Показать все сообщения

пятница, 8 мая 2015 г.

Красивый assertThat в стиле Mockito

На ночь глядя взялся реализовать двусвязный список на Java. Пишу тест
    @Test
    public void testAddAtAtEnd() {
        List<String> list = get();

        list.add("1");
        assertEquals("[1]", list.toString());

        list.add("2");
        assertEquals("[1, 2]", list.toString());

        list.add("3");
        assertEquals("[1, 2, 3]", list.toString());

        list.add("4");
        assertEquals("[1, 2, 3, 4]", list.toString());
    }
Неуклюже и некрасиво! Хочу так!!
    @Test
    public void testAddAtAtEnd() {
        List<String> list = get();

        assertAfter("[1]", list).add("1");
        assertAfter("[1, 2]", list).add("2");
        assertAfter("[1, 2, 3]", list).add("3");
        assertAfter("[1, 2, 3, 4]", list).add("4");
        assertAfter("[1, 2, 3, 4, 5]", list).add("5");
        assertAfter("[1, 2, 3, 4, 5, 6]", list).add("6");
        assertAfter("[1, 2, 3, 4, 5, 6, 7]", list).add("7");
    }
Для этого мне надо вот
    private <T> T assertAfter(final String expected, final T object) {
        return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(),
                object.getClass().getInterfaces(),
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object invoke = method.invoke(object, args);
                        assertEquals(expected, object.toString());
                        return invoke;
                    }
                });
    }
Приятного!

вторник, 17 июня 2014 г.

Как мокать Bean'ы внутри Spring context запущеного Jetty? Part 2

Есть Web приложение на Spring (MVC + IoC). Есть Jetty сервер, на котором оно ранится. Web приложение в своей Model содержит ряд сервисов (Spring Beans), которыми пользуются контроллера.

Задача: Написать функциональный тест с испольованием WebDriver, но так, чтобы из теста была возможность мокать реальные сервиса модели.

В прошлый раз задача решилась, но как показала практика - сервиса, которые autowired'ся в другие сервиса не мокаются, мокаются только бины, которые попадают во внешние классы, например, вебконтроллеры, или при получении бина из context.getBean("name");

Попробуем решить и эту задачку. Допустим есть два бина:
package com.services;

import org.springframework.stereotype.Component;

@Component("otherService")
public class OtherService {
    
    public String getOtherString() {
        return "Hello from " + OtherService.class.getSimpleName() + "!";
    }
}
И
package com.services;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component("service")
public class Service {

    @Autowired
    private OtherService otherService;

    public String getString() {
        return "Hello from " + Service.class.getSimpleName() + "! " + otherService.getOtherString();
    }
}
Как видно по исходному коду - они зависимы друг от дружки.

Есть еще SpringMVC контроллер
package com.controller;

import com.services.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/")
public class SomeController {

    @Autowired
    private Service service;

    @RequestMapping(method = RequestMethod.GET)
    public String action(Model model) {
        model.addAttribute("string", service.getString());
        return "view";
    }
}
Все как видно работает на аннотациях, о чем указано в идеально чистом applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

    <context:annotation-config/>
    <context:component-scan base-package="com.services"/>

</beans>
Есть и вьюшка (но это не так важно, потому как она всего лишь выводит в html то, что ей передали)
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html;">
    <title>Main page</title>
</head>
<body>
    <c:if test="${string!=null}">
        <span id="message">${string}</span>
    </c:if>
<body>
</html>
Есть еще два конфига servlet-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
            http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd">

    <context:annotation-config/>
    <context:component-scan base-package="com.controller"/>

    <mvc:annotation-driven/>

    <mvc:resources mapping="/resources/**" location="/resources/" />

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/view/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>
Ну и конечно же сердце web приложения web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         id="WebApp_ID" version="3.0">
    <display-name>Mocker</display-name>

    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.XmlWebApplicationContext</param-value>
    </context-param>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:com/applicationContext.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/servlet-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>
И как теперь сделать так, чтобы я в тестах, которые пишу мог мокать любой из бинов, при этом поднимая приложение на реальном allicationContext.xml не влезая тестами в продакшен код? Оказывается (спустя 5 часов ночного дебага) не так уж и сложно. История начинается с небольшого интерфейса BeanPostProcessor, реализовав который можно встроиться в процесс инициализации каждого бина в системе. Он прост.
 * Copyright 2002-2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.beans.factory.config;

import org.springframework.beans.BeansException;

/**
 * Factory hook that allows for custom modification of new bean instances,
 * e.g. checking for marker interfaces or wrapping them with proxies.
 *
 * ApplicationContexts can autodetect BeanPostProcessor beans in their
 * bean definitions and apply them to any beans subsequently created.
 * Plain bean factories allow for programmatic registration of post-processors,
 * applying to all beans created through this factory.
 *
 * Typically, post-processors that populate beans via marker interfaces
 * or the like will implement {@link #postProcessBeforeInitialization},
 * while post-processors that wrap beans with proxies will normally
 * implement {@link #postProcessAfterInitialization}.
 *
 * @author Juergen Hoeller
 * @since 10.10.2003
 * @see InstantiationAwareBeanPostProcessor
 * @see DestructionAwareBeanPostProcessor
 * @see ConfigurableBeanFactory#addBeanPostProcessor
 * @see BeanFactoryPostProcessor
 */
public interface BeanPostProcessor {
    /**
     * Apply this BeanPostProcessor to the given new bean instance <i>before</i> any bean
с * initialization callbacks (like InitializingBean's <code>afterPropertiesSet</code>
     * or a custom init-method). The bean will already be populated with property values.
     * The returned bean instance may be a wrapper around the original.
     * @param bean the new bean instance
     * @param beanName the name of the bean
     * @return the bean instance to use, either the original or a wrapped one; if
     * <code>null</code>, no subsequent BeanPostProcessors will be invoked
     * @throws org.springframework.beans.BeansException in case of errors
     * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet
     */
    Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
    
        /**
     * Apply this BeanPostProcessor to the given new bean instance <i>after</i> any bean
     * initialization callbacks (like InitializingBean's <code>afterPropertiesSet</code>
     * or a custom init-method). The bean will already be populated with property values.
     * The returned bean instance may be a wrapper around the original.
     * In case of a FactoryBean, this callback will be invoked for both the FactoryBean
     * instance and the objects created by the FactoryBean (as of Spring 2.0). The
     * post-processor can decide whether to apply to either the FactoryBean or created
     * objects or both through corresponding <code>bean instanceof FactoryBean</code> checks.
     * This callback will also be invoked after a short-circuiting triggered by a
     * {@link InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation} method,
     * in contrast to all other BeanPostProcessor callbacks.
     * @param bean the new bean instance
     * @param beanName the name of the bean
     * @return the bean instance to use, either the original or a wrapped one; if
     * <code>null</code>, no subsequent BeanPostProcessors will be invoked
     * @throws org.springframework.beans.BeansException in case of errors
     * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet
     * @see org.springframework.beans.factory.FactoryBean
     */
    Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
Один его метод postProcessAfterInitialization - нам и нужен. Именно в нем можно обернуть тот или иной бин в Mockito.spy() и получить желаемый результат. Чтобы BeanPostProcessor заработал, его так же надо (либо аннотациями либо через applicationContext.xml) засветить перед Spring.
@Component // вот без этого пока никак! Процессор должен быть виден для Spring
public class SpyPostProcessor implements BeanPostProcessor, Ordered {

        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            return bean;
        }

        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            if (spy.equals("service")) { // как-то так
                return Mockito.spy(bean);
            } else {
                return bean;
            }
        }

        @Override
        public int getOrder() {
            return Ordered.LOWEST_PRECEDENCE; // наивысший приоритет у этого BeanPostProcessor
        }
    }
Так же желательно реализовать еще один интерфейс, чтобы указать порядок обработки бина в очереди других зарегистрированных BeanPostProcessor'ов.
/*
 * Copyright 2002-2009 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.core;

/**
 * Interface that can be implemented by objects that should be
 * orderable, for example in a Collection.
 *
 * The actual order can be interpreted as prioritization, with
 * the first object (with the lowest order value) having the highest
 * priority.
 *
 * Note that there is a 'priority' marker for this interface:
 * {@link PriorityOrdered}. Order values expressed by PriorityOrdered
 * objects always apply before order values of 'plain' Ordered values.
 *
 * @author Juergen Hoeller
 * @since 07.04.2003
 * @see OrderComparator
 * @see org.springframework.core.annotation.Order
 */
public interface Ordered {

    /**
     * Useful constant for the highest precedence value.
     * @see java.lang.Integer#MIN_VALUE
     */
    int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;

    /**
     * Useful constant for the lowest precedence value.
     * @see java.lang.Integer#MAX_VALUE
     */
    int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

    /**
     * Return the order value of this object, with a
     * higher value meaning greater in terms of sorting.
     * Normally starting with 0, with <code>Integer.MAX_VALUE</code>
     * indicating the greatest value. Same order values will result
     * in arbitrary positions for the affected objects.
     * Higher values can be interpreted as lower priority. As a
     * consequence, the object with the lowest value has highest priority
     * (somewhat analogous to Servlet "load-on-startup" values).
     * @return the order value
     */
    int getOrder();

}
Хорошо, это решает наш вопрос с оборачиванием бинов, но вовсе не решает другую часть задачи - "не лезть в целях тестов в production код". Как быть? Будем хачить... Я узнал, что есть некто BeanFactory - реализации которого участвуют в построении бинов. Один из его наследников ConfigurableBeanFactory содержит метод addBeanPostProcessor, с помощью которого можно добавить в систему "ручками" свой BeanPostProcessor. Вот его описание
    /**
     * Add a new BeanPostProcessor that will get applied to beans created
     * by this factory. To be invoked during factory configuration.
     * Note: Post-processors submitted here will be applied in the order of
     * registration; any ordering semantics expressed through implementing the
     * {@link org.springframework.core.Ordered} interface will be ignored. Note
     * that autodetected post-processors (e.g. as beans in an ApplicationContext)
     * will always be applied after programmatically registered ones.
     * @param beanPostProcessor the post-processor to register
     */
    void addBeanPostProcessor(BeanPostProcessor beanPostProcessor);
Теперь мне нужно место, в котором создается BeanFactory чтобы сразу после ее инициализации я добавил бы свой BeanPostProcessor в ее нутро. Где? Дядюшка Дебаг поможет! В итоге я пришел к самому главному XmlWebApplicationContext, который содержит в себе метод createBeanFactory. Занаследовавшись от него я получаю возможность реализовать желаемое
public class SpyXmlWebApplicationContext extends XmlWebApplicationContext {

    @Override
    protected DefaultListableBeanFactory createBeanFactory() {
        DefaultListableBeanFactory factory = super.createBeanFactory();
        factory.addBeanPostProcessor(new SpyPostProcessor());
        return factory;
    }
}
Мы уже близко к цели - теперь осталось поменять класс контекста в web.xml с XmlWebApplicationContext на мой SpyXmlWebApplicationContext.
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         id="WebApp_ID" version="3.0">
    <display-name>Mocker</display-name>

    <context-param>
        <param-name>contextClass</param-name>
        <param-value>intergation.SpyXmlWebApplicationContext</param-value> 
    </context-param>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:com/applicationContext.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
...
Но мы жеж договорились, что никаких грязных рук в production коде. А потому идем к нашему jetty и будем просить его нам помочь. Если мы добавим ServletContextListener лиснер для загруженного WebAppContext, то во время его обработки можно будет изменить все эти context-param свойства на значения, которые нам нужны.
package integraion;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class JettyRunner {
    public static int start() throws Exception {
        Server server = new Server(0);
        final WebAppContext context = new WebAppContext("src/main/webapp", "/mocker");
        context.addEventListener(new ServletContextListener() {
            @Override
            public void contextInitialized(ServletContextEvent sce) {
                String contextClass = context.getInitParameter(ContextLoader.CONTEXT_CLASS_PARAM);
                if (!contextClass.equals(XmlWebApplicationContext.class.getName())) {
                    throw new RuntimeException("Тип " + contextClass  + " не поддерживается!"); // да-да :)
                }
                context.setInitParameter(ContextLoader.CONTEXT_CLASS_PARAM, SpyXmlWebApplicationContext.class.getName()); // заменяем на свой
            }
        });
        server.setHandler(context);
        server.start();
        int port = server.getConnectors()[0].getLocalPort();
        return port;
    }
}
Все, паззл собрался. Теперь осталось добавить возможности конфигурировать это все дело - потому как у нас если помнишь имя бина захардкоджено. Но это уже не так важно - тем более, что в этих исходниках, я как бы все уже причесал. Качай на зоровье!

четверг, 30 января 2014 г.

Хотелось выпендрится

Чем люблю проект выходного дня - тем, что тут я могу рефакторить без конца и придумывать новые комбинации строк кода, порой даже нелепые. Это все позволяет мне знать как можно, а как не стоит. Сейчас я предложу пример того как можно было бы, но не стоит :)

Пишу на ночь глядя эмулятор КР580ВМ80 - ну такое, just for fun. Конечно же по ТДД. Тесты рефакторю, как положено. И вот я в тесте пишу как-то так ассерт
    assertEquals("DEh", registry.m().toString());
При том, что
public class ProcessorTest {

    private Memory memory;
    private Processor processor;
    private Registry registry;

    @Before
    public void setup() {
        memory = mock(Memory.class);
        processor = new Processor(memory);
        registry = processor.registry();
    }
а интерфейс
public interface Registry {
    AByte b();
    AByte c();
    AByte d();
    AByte e();
    AByte h();
    AByte l();
    AByte m();
    AByte a();
}
а AByte содержит че-то умное и toString. Но мне хотелось выпендрится и сделать вот так в тесте, чтобы на at(). вываливались аутокомплитером IDE методы интерфейса Registry


Проверил, может ли такое Mockito. Оказалось может!
    private RegistryAssert assertValue(String data) {
        return new RegistryAssert(data);
    }
    
    class RegistryAssert {

        private String data;

        public RegistryAssert(String data) {
            this.data = data;
        }

        public Registry at() {
            // создаем шпиона для registry
            Registry spy = spy(registry); 
            // и следим за каждым его чихом
            setup(spy).b(); 
            setup(spy).c();
            setup(spy).d();
            setup(spy).e();
            setup(spy).h();
            setup(spy).l();
            setup(spy).m();
            setup(spy).a();
            return spy;
        }

        private Registry setup(Registry spy) {
            return doAnswer(new Answer<Object>() {
                @Override
                public Object answer(InvocationOnMock invocation) throws Throwable {
                    Object actual = invocation.callRealMethod();
                    assertEquals(data, actual.toString());
                    return actual;
                }
            }).when(spy);
        }
    }
Не делайте так ☺

воскресенье, 18 ноября 2012 г.

Как мокать Bean'ы внутри Spring context запущеного Jetty?

Есть Web приложение на Spring (MVC + IoC). Есть Jetty сервер, на котором оно ранится. Web приложение в своей Model содержит ряд сервисов (Spring Beans), которыми пользуются контроллера.

Задача: Написать функциональный тест с испольованием WebDriver, но так, чтобы из теста была возможность мокать реальные сервиса модели.

Updated: Есть более продвинутая имплементация этой задачи, вот тут. Все что дальше по тексту этого поста - на память :)

Например в приложении есть 1 сервис MyService
package com.services;

import org.springframework.stereotype.Component;

/**
 * User: apofig
 * Date: 11/18/12
 * Time: 9:49 PM
 */
@Component("myService")
public class MyService {

    public String getString() {
        return "Hello from " + MyService.class.getSimpleName() + "!";
    }

}
Он инджектится в контроллер MyController с помощью @Autowired аннотации Spring'ом.
package com.controller;

import com.services.MyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * User: apofig
 * Date: 9/20/12
 * Time: 1:37 PM
 */
@Controller
@RequestMapping("/")
public class MyController {

    @Autowired
    private MyService service;

    @RequestMapping(method = RequestMethod.GET)
    public String savePlayerGame(Model model) {
        model.addAttribute("string", service.getString());
        return "view";
    }
}
Контроллер отправляет данные на jsp, которая рисует сообщение на экране
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html;">
    <title>Main page</title>
</head>
<body>
    <c:if test="${string!=null}">
        <span id="message">${string}</span>
    </c:if>
<body>
</html>
Во время старта приложения я хочу, чтобы там был не реальный MyService, а его мок (или шпион), которым я буду рулить из теста вот так
public class IntegrationTest {
    private String url = "http://localhost:8080/";
    private WebDriver driver = new HtmlUnitDriver(true);
    private MyService service;

    @Test
    public void shouldGetMessageFromService() throws IOException, InterruptedException {
        when(service.getString()).thenReturn("Hi from mock!");

        driver.get(url);

        String message = driver.findElement(By.id("message")).getText();
        assertEquals("Hi from mock!", message);
    }
}
Зачем это делать? Если писать полностью функиональные тесты, тогда чтобы протестить, скажем, удаление пользователя из системы надо будет вначале создать этого пользователя по всем правилам. Но если иметь возможность мокать модель, тогда лишние действия не потребуются - мы всего лишь проверим, что до модели дошел запрос о удалении юзера и все.

Итак я долго (дня два) искал возможность это сделать. Google почти молчит. Самое близкое, что я нашел - библиотека Springockito, но нашел я ее тогда, когда практически моя версия была рабочей.

Как я решал проблему? При старте Jetty сервера есть возможность подслушать момент инициплизации контекста.
    Server server = new Server(0);
    WebAppContext context = new WebAppContext("src/main/webapp", "");
    context.addEventListener(new ServletContextListener() {
        @Override
        public void contextInitialized(ServletContextEvent sce)
            ServletContext servletContext = sce.getServletContext(); 
            // вот тут       
        }
    }
    server.setHandler(context);
    server.start();
    int port = server.getConnectors()[0].getLocalPort();
    String url = "http://localhost:" + port;
Я так же знаю, что после окончательного старта сервера в ServletContext будет находиться Spring'овый WebApplicationContext, но в момент отработки ServletContextListener его там еще нет.

После старта сервера я могу его получить вот так
    WebApplicationContext applicationContext = 
        WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
Имея на руках Spring'овый контекст я могу поковырять его - могу удалить существующий бин и заменить его моком, так что всякий раз когда клиент будет делать context.getBean(name) он будет получать мой мок. Вперед!
    private static Object mocking(WebApplicationContext webAppContext, String beanName, boolean isMockOfSpy) {
        Object realBean = webAppContext.getBean(beanName);

        AbstractRefreshableApplicationContext context = (AbstractRefreshableApplicationContext) webAppContext;
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();

        Object newBean = null;
        if (isMockOfSpy) {
            newBean = mock(realBean.getClass());
        } else {
            newBean = spy(realBean);
        }

        beanFactory.removeBeanDefinition(beanName);
        beanFactory.registerSingleton(beanName, newBean);
        return beanFactory.getBean(beanName);
    }
Конечно, тут не помешает пару проверок (для ClassCast), потому как контекст мог быть не обязательно XmlWebApplicationContext как у меня - а там иди знай, что вернется.

Но это не все. В момент, когда я могу провернуть такую махинацию Spring уже проинджектил все бины в поля зависимых классов по аннотации @Autowired. И хоть я могу получить мок с помощью context.getBean(name), меня это не устраивает.

Несколько часов дебага с кондишенал брейкопинтами и я поймал момент, когда WebApplicationContext уже имеет на руках все бины, но еще не успел расставить их куда велено. Любопытно, что этот момент - такой же как и мой обработчик ServletContextListener.

В момент когда я добавляю свой обработчик, список обработчиков WebAppContext пуст (т.е. я первым слушаю) - тут нигде нет никакого WebApplicationContext. Даже в момент, когда мой обработчик отрабатывает, WebApplicationContext'а так же нигде нет.
    Server server = new Server(0);
    WebAppContext context = new WebAppContext("src/main/webapp", "");
    // Spring контекст недуступен
    context.addEventListener(new ServletContextListener() {
        @Override
        public void contextInitialized(ServletContextEvent sce)
            ServletContext servletContext = sce.getServletContext();        
            // Spring контекст так же недуступен
        }
    }
    server.setHandler(context);
    server.start();
    int port = server.getConnectors()[0].getLocalPort();
    String url = "http://localhost:" + port;
Обыно он хранится в _contextAttributes, но тут пока пусто
Но я точно знаю, что он там будет спустя некоторое время (ибо додебажился).
Через некоторое время, в очередь, после моего ServletContextListener обработчика, выстрится ряд других обработчиков. Последним из них будет Spring'овый. И вот когда он отработает, то WebApplicationContext появится внутри ServletContext.В этот момент бины будут уже готовые, но еще пока не расставленные по зависимым классам.

Мне надо как-то стать пятым в очереди, и сделал я это так
    private ServletContext servletContext;
    private WebApplicationContext applicationContext;

    public String start() throws Exception {
        server = new Server(0);
        final WebAppContext context = loadWebContext();
        context.addEventListener(new ServletContextListener() {
            @Override
            public void contextInitialized(ServletContextEvent sce) {
                servletContext = sce.getServletContext();

                context.addEventListener(new ServletContextListener() {
                    @Override
                    public void contextInitialized(ServletContextEvent sce) {
                        applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
                        // тут бины тепленькие, и все мои!!!
                    }

                    @Override
                    public void contextDestroyed(ServletContextEvent sce) {
                    }
                });
            }

            @Override
            public void contextDestroyed(ServletContextEvent sce) {
            }
        });
        server.setHandler(context);
        server.start();
        int port = server.getConnectors()[0].getLocalPort();

        String url = "http://localhost:" + port;
        return url;
    }
Дальше дело техники, вызываем метод
    private static Object mocking(WebApplicationContext webAppContext, String beanName, boolean isMockOfSpy) {
И сразу после того, как отработчик отработает Spring полезет деливерить бины по зависимым клиентам (@Autowired).

Я в дамках!

Вот исходный код простенького Spring Web проекта с демонстрацией этой возможности.

воскресенье, 27 ноября 2011 г.

Java for fun: Как работать с Mockito?

Не так давно, на двухдневном тренинге по TDD я решил поизучать новый mock-framework. Путем гугления выбрал самую популярную. 4 часа я вкуривал доку по Mockito v1.8.5. Читал и писал тесты, чтобы опробовать либу на деле.

В доке все супер понятно написано, но знание, которое не опробовано на практике скоро теряется. Так родился этот проектик с несколькими десятками тестов, проверяющих почти все возможности библиотеки.

Еще пол дня и я могу представить презенташку основанную на этих простых тестах



Пользуйся на здоровье. Надеюсь мой день не прошел даром и ты получишь ответы на вопросы:
- Как работать с Mockito?
- В чем фишка Mockito?
- Что лучше: Mockito или другой mock-framework?