понедельник, 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.
Вот и все. Как видите нет ничего сложного, тесты проходят успешно, как мы и ожидали.

1 комментарий: