понедельник, 31 октября 2011 г.

Тестирование GWT с помощью Mockito

    Иногда в процессе разработки возникает необходимость использования интерфейса, до того, как он будет реализован. Тогда нам на помощь приходят mock-обьекты, которые предоставляют фиктивную реализацию интерфейса. Они заменяют реальный объект во время теста и позволяют имитировать поведения.
      Mockito - это очень интересный фреймворк для mock-тестирования, который позволяет писать красивые и понятные тесты. Про основные возможности и особенности можно прочитать по след. ссылке. В процессе тестирования мы будем использовать класс GWTMockUtilities, который позволит имитировать классы GWT используя Mockito как в JUnit тестах. Что же произойдет если мы не будет использовать этот класс? При запуске теста мы получим след. ошибку:
java.lang.ExceptionInInitializerError
    at sun.reflect.GeneratedSerializationConstructorAccessor1.newInstance(Unknown Source)
    at java.lang.reflect.Constructor.newInstance(Unknown Source)
    at org.objenesis.instantiator.sun.SunReflectionFactoryInstantiator.newInstance(SunReflectionFactoryInstantiator.java:40)
    at org.objenesis.ObjenesisBase.newInstance(ObjenesisBase.java:59)
    at org.mockito.internal.creation.jmock.ClassImposterizer.createProxy(ClassImposterizer.java:111)
    …...
Caused by: java.lang.UnsupportedOperationException: ERROR: GWT.create() is only usable in client code! It cannot be called, for example, from server code. If you are running a unit test, check that your test case extends GWTTestCase and that GWT.create() is not called from within an initializer or constructor.
    at com.google.gwt.core.client.GWT.create(GWT.java:92)
    at com.google.gwt.user.client.ui.UIObject.<clinit>(UIObject.java:188)
    ... 35 more


* This source code was highlighted with Source Code Highlighter.
     Метод GWT.create() вызывается при инициалиализации статического поля UiObject, это происходит, когда JVM загружает класс TextBox. Сообщение об исключении рекомендует чтобы тест наследовался от GWTTestCase, случай интеграционного теста, который запускает JavaScript в хостинг режиме браузера. Но в нашем тесте мы не используем настоящее текстовое поле, мы просто хотим создать копию. Так почему же мы должны тратить время при выполнении теста в медленном GWTTestCase? 
     GWTMockUtilities предоставляет способ решения этой проблемы. Через обертывание тестовых методов с GWTMockUtilities.disarm() и restore(), мы можем временно отключить побочный эффект вызова GWT.create() при статической инициализации. 
     Пример
у нас есть определенная view - ReceiverWidget с текстовым полем и методами для установки и получения значения
public class ReceiverWidget extends Composite implements IReceiverWidget {
    private static ReceiverWidgetUiBinder uiBinder = GWT.create(ReceiverWidgetUiBinder.class);

    @UiTemplate("ReceiverWidget.ui.xml")
    interface ReceiverWidgetUiBinder extends UiBinder<Widget, ReceiverWidget> {
    }

    @UiField TextBox description;
    // other fields

    public ReceiverWidget() {
        initWidget(uiBinder.createAndBindUi(this));
    }

    public void setDescription(String text) {
        description.setText(text);
    }

    public String getDescription() {
        return description.getText();
    }

    // other methods
}


* This source code was highlighted with Source Code Highlighter.
и виджет GameBox у которого есть кнопка, но которую можно навешать ClickHandler
public class GameBox extends Composite {

    @UiTemplate("GameBox.ui.xml")
    interface GameBoxUiBinder extends UiBinder<Widget, GameBox>{}

    public static GameBoxUiBinder uiBinder = GWT.create(GameBoxUiBinder.class);

    @UiField Button ok;
    // other fields    

    public GameBox() {
        this.initWidget(uiBinder.createAndBindUi(this));
    }

    public HasClickHandlers getOkButton() {
        return ok;
    }

    public void showWindowAlert() {
        Window.alert("Mockito");
    }

    // other methods
}


* This source code was highlighted with Source Code Highlighter.
     Протестируем класс ReceiverWidget
public class ReceiverWidgetTest {
    private ReceiverWidget receiverWidget;

    @Before
    public void disarm() {
        GWTMockUtilities.disarm();
        receiverWidget = mock(ReceiverWidget.class, CALLS_REAL_METHODS);
   
        receiverWidget.description = mock(TextBox.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS));
    }

    @After
    public void restore() {
        GWTMockUtilities.restore();
    }

    @Test
    public void testSetDescription() {
        String testValue = "test";
   
        receiverWidget.setDescription(testValue);
        verify(receiverWidget.description).setText(testValue);
    }

    @Test
    public void testGetDescription() {
        String testValue = "test";
   
        Mockito.when(receiverWidget.description.getText()).thenReturn(testValue);
        String value = receiverWidget.description.getText();
        verify(receiverWidget.description).getText();
        assertEquals(testValue, value);
    }
}


* This source code was highlighted with Source Code Highlighter.
     Класс GameBox используется в ReceiverPresenter презентере, мы просто навешиваем обработчик ClickHandler на его кнопку
public class ReceiverPresenter extends AbstractGroupPresenter<IReceiverWidget, AppEventBus> {

    public interface IReceiverWidget {
        void showGroup(String group);
        void setReceivedValue(String value);
    }

    GameBox gameBox;

    @Override
    public void bind() {
        view.showGroup(getGame());
   
        gameBox.getOkButton().addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
                gameBox.showWindowAlert();
            }
        });

        // …
    }

    // other methods
}


* This source code was highlighted with Source Code Highlighter.
     Давайте напишем класс ReceiverPresenterTest который протестирует наш презентер
public class ReceiverPresenterTest {

    private ReceiverPresenter presenter;
    private AppEventBus eventBus;
    private IReceiverWidget view;

    /**
     * method to simulate clicking
     *
     * @return
     */
    public static Answer<?> createClickEmulator() {
        Answer<?> clickEmulator = new Answer<Object>() {
            public Object answer(InvocationOnMock invocation) throws Throwable {
                ClickHandler clickHandler = (ClickHandler) invocation.getArguments()[0];
                clickHandler.onClick(null);
                return null;
            }
        };
        return clickEmulator;
    }

    @Before
    public void disarm() {
        GWTMockUtilities.disarm();
   
        presenter = mock(ReceiverPresenter.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));

        eventBus = mock(AppEventBus.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
        presenter.setEventBus(eventBus);
        view = mock(IReceiverWidget.class, RETURNS_DEEP_STUBS);
        presenter.setView(view);
        presenter.gameBox = mock(GameBox.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS));
    }

    @After
    public void restore() {
        GWTMockUtilities.restore();
    }

    @Test
    public void testBind() {
        presenter.bind();

        verify(presenter.gameBox.getOkButton()).addClickHandler((ClickHandler) any());
    }

    @Test
    public void testBindOkButtonClick() {
        when(presenter.gameBox.getOkButton().addClickHandler((ClickHandler) any())).thenAnswer(createClickEmulator());
   
        presenter.bind();
   
        verify(presenter.gameBox).showWindowAlert();
    }
}


* This source code was highlighted with Source Code Highlighter.
Вот и все. Как видите нет ничего сложного, тесты проходят успешно, как мы и ожидали.

вторник, 18 октября 2011 г.

GWT & Mvp4G Event Filter

    В продолжение статьи об GWT + Mvp4G рассмотрим включение/отключение обработчиков с помощью Mvp4G Event Filter. Mvp4g позволяет фильтровать события и останавливать до того как они будут переданы обработчикам. Избавимся от условий if на каждое событие в группах презентеров. Сначала создадим PresenterHandler, который будет отвечать за инициализацию презентеров, добавление их в группы отправителей/получателей и активации списка презентеров для группы событий.
@EventHandler
@Singleton
public class PresenterHandler extends BaseEventHandler<AppEventBus> {

    private Map<Game, List<AbstractGroupPresenter<?, AppEventBus>>> presenterMap;

    public PresenterHandler() {
        presenterMap = new HashMap<Game, List<AbstractGroupPresenter<?, AppEventBus>>>();
    }

    public void onInit() {
        addWidget(SenderPresenter.class, Game.Action);
        addWidget(ReceiverPresenter.class, Game.Action);
        addWidget(ReceiverPresenter.class, Game.Action);
        addWidget(ReceiverPresenter.class, Game.Action);

        addWidget(SenderPresenter.class, Game.RPG);
        addWidget(ReceiverPresenter.class, Game.RPG);
        addWidget(ReceiverPresenter.class, Game.RPG);
        addWidget(ReceiverPresenter.class, Game.RPG);

        addWidget(SenderPresenter.class, Game.Strategy);
        addWidget(ReceiverPresenter.class, Game.Strategy);
        addWidget(ReceiverPresenter.class, Game.Strategy);
        addWidget(ReceiverPresenter.class, Game.Strategy);
    }

    private void addWidget(
            Class<? extends AbstractGroupPresenter<?, AppEventBus>> component, Game game) {
        AbstractGroupPresenter<?, AppEventBus> presenter = (AbstractGroupPresenter<?, AppEventBus>) eventBus
                .addHandler(component, false);
        presenter.setGame(game.toString());
        presenter.bind();
        if (!presenterMap.containsKey(game)) {
            presenterMap.put(game, new ArrayList<AbstractGroupPresenter<?, AppEventBus>>());
        }
        presenterMap.get(game).add(presenter);
    
        if (presenter instanceof SenderPresenter) {
            eventBus.showSenderWidget((Widget) presenter.getView());
        } else {
            eventBus.showReceiverWidget((Widget) presenter.getView());
        }
    }

    /**
     * activate the presenter list for an event group
     */
    public void activateGroup(Game groupToActivate) {
        for (Game game : presenterMap.keySet()) {
            for (AbstractGroupPresenter<?, AppEventBus> presenter : presenterMap.get(game)) {
                // when activating an empty group, activate all presenter
                if (groupToActivate == null || groupToActivate.equals("") || groupToActivate.equals(game)) {
                    presenter.setActivated(true);
                } else {
                    presenter.setActivated(false);
                }
            }
        }
    }
}


* This source code was highlighted with Source Code Highlighter.
     Далее создадим фильтр (AppPresentersEventFilter), который будет срабатывать на каждое событие вызванное в нашей шине событий (AppEventBus).
public class AppPresentersEventFilter implements EventFilter<AppEventBus> {

    @Inject
    private PresenterHandler presenterHandler;

    @Override
    public boolean filterEvent(String eventName, Object[] params, AppEventBus eventBus) {
        // deactivate presenter registered for a group when the event name ends in "$"
        if (eventName.endsWith("$")) {
            presenterHandler.activateGroup(Game.valueOf((String) params[0]));
        }
    
        // always return true to forward event to all active handlers
        return true;
    }
}


* This source code was highlighted with Source Code Highlighter.
     И подключим к шине событий:
@Filters(filterClasses = AppPresentersEventFilter.class)
@Events(startView = MainWidget.class)
public interface AppEventBus extends EventBus {

    @Start
    @Event(handlers = { PresenterHandler.class })                        
    public void init();

    @Event(handlers = { MainPresenter.class })
    public void showSenderWidget(IsWidget widget);

    @Event(handlers = { MainPresenter.class })
    public void showReceiverWidget(IsWidget widget);

    @Event(handlers = {ReceiverPresenter.class})
    public void setSelectedItem$(String genre, String value);
}


* This source code was highlighted with Source Code Highlighter.
     Также можно динамически добавлять/удалять фильтр с помощью методов addEventFilter и removeEventFilter в шине событий.
AppPresentersEventFilter filter = new AppPresentersEventFilter();
eventBus.addEventFilter(filter);
eventFilter.removeEventFilter(filter);


* This source code was highlighted with Source Code Highlighter.
     В шине используется метод setSelectedItem$(String genre, String value) с особым символом для фильтрации событий. Когда происходит событие и срабатывает фильтр, мы делаем проверку окончания имени события. Если имя заканчивается на символ “$”, то отключаем зарегистрированную группу презентеров. В фильтре происходит инжект PresenterHandler, через который вызываем метод activateGroup(Game groupToActivate) для включения/отключения списка презентеров для группы событий.
     Презентеры (Main, Receiver and Sender) после изменений выглядят вот так:
@Presenter(view = MainWidget.class)
public class MainPresenter extends BasePresenter<IMainWidget, AppEventBus> {

    public interface IMainWidget {
        public void addSenderWidget(IsWidget widget);
        public void addReceiverWidget(IsWidget widget);
    }

    public void onShowSenderWidget(IsWidget widget) {
        view.addSenderWidget(widget);
    }

    public void onShowReceiverWidget(IsWidget widget) {
        view.addReceiverWidget(widget);
    }
}


* This source code was highlighted with Source Code Highlighter.
@Presenter(view = ReceiverWidget.class, multiple = true)
public class ReceiverPresenter extends AbstractGroupPresenter<IReceiverWidget, AppEventBus> {

    public interface IReceiverWidget {
        void showGroup(String group);
        void setReceivedValue(String value);
    }

    @Override
    public void bind() {
        view.showGroup(getGame());
    }

    public void onSetSelectedItem$(String genre, String value) {
        List<String> games = Genre.valueOf(value).getGames();
        view.setReceivedValue(games.get(Random.nextInt(games.size())));
    }
}


* This source code was highlighted with Source Code Highlighter.
@Presenter(view = SenderWidget.class, multiple = true)
public class SenderPresenter extends AbstractGroupPresenter<ISenderWidget, AppEventBus> {
    public interface ISenderWidget {
        void showGroup(String group);
        HasChangeHandlers getSelectableComponent();
        String getSelectedValue();
        void displayValues(List<Genre> asList);
    }

    public void bind() {
        view.showGroup(getGame());
        view.getSelectableComponent().addChangeHandler(new ChangeHandler() {
            @Override
            public void onChange(ChangeEvent event) {
                eventBus.setSelectedItem$(getGame(), view.getSelectedValue());
            }
        });
        view.displayValues(Game.valueOf(getGame()).getGenre());
    }
}


* This source code was highlighted with Source Code Highlighter.
     Вот и все. Eclipse проект можно скачать по следующей ссылке.