19 января 2012 г.

Обзор EasyMock

Юнит-тестинг - тестирование классов или их методов в изоляции от других объектов системы. Обычно классы в Java зависят от других классов, таким образом чтбы протестировать какую-либо функциональность вам нужно будет создать экземлпяр класса, отличного от тестируемого, что само по себе противоречит идее юнит-тестинга. Mock-объект - интерфейс или класс-пустышка, в котором вы определяете результат вызова методов. Эти объекты и передаются классу, который мы хотим протестировать, т.к. при юнит-тестинге мы должны избегать зависимостей от любых внешних данных.

Мы можем писать такие классы-заглушки сами, а можем воспользоваться одним из Mock-фреймворков для симуляции таких классов.

В Java к самым популярным Mock-фреймворкам можно отнести EasyMock и jMock. Сегодня мы рассмотрим первый из них.



Подключение EasyMock к проекту
Предположим вы используете maven. Сначала вам необходимо прописать зависимоть от библиотеки EasyMock в вашем проекте, сделать это можно так:
<dependency>
    <groupId>org.easymock</groupId>
    <artifactId>easymock</artifactId>
    <version>3.0</version>
    <scope>test</scope>
</dependency>

Работа с EasyMock
Теперь попробуем что-нибудь протестировать. Допустим у нас есть некий интерфейс Collaborator:
package org.easymock.samples;

public interface Collaborator {
    void documentAdded(String title);
    void documentChanged(String title);
    void documentRemoved(String title);
    byte voteForRemoval(String title);
    byte[] voteForRemovals(String[] title);
}
И есть класс, который использует этот интерфейс:
public class ClassUnderTest {
    // ...
    public void addListener(Collaborator listener) {
        // ...
    }
    public void addDocument(String title, byte[] document) {
        // ...
    }
    public boolean removeDocument(String title) {
        // ...
    }
    public boolean removeDocuments(String[] titles) {
        // ...
    }
}
Приступим к тестированию. Наш первый тест должен проверить, что удаление несуществуюего документа НЕ вызывает нотификацию для слушателей (экземпляров классов-реализаций интерфейса Collaborator, которые подписались на события экземпляра класса ClassUnderTest). Вот тест без определения Mock-объекта:
package org.easymock.samples;
import org.junit.*;

public class ExampleTest {

    private ClassUnderTest classUnderTest;
    private Collaborator mock;

    @Before
    public void setUp() {
        classUnderTest = new ClassUnderTest();
        classUnderTest.addListener(mock);
    }

    @Test
    public void testRemoveNonExistingDocument() {
        // This call should not lead to any notification
        // of the Mock Object:
        classUnderTest.removeDocument("Does not exist");
    }
}
Часто для тестрования с EasyMock, вы должны импортировать методы org.easymock.EasyMock:
import org.easymock.EasyMock.*;

Для работы с Mock-объектом нужно:
  1. Создать экземпляр Mock-объекта для тестируемого интерфейса
  2. Указать ожидаемое поведение
  3. Переключить Mock-объект в состояние репликации
Пример:
@Before
public void setUp() {
    mock = createMock(Collaborator.class); // 1
    classUnderTest = new ClassUnderTest();
    classUnderTest.addListener(mock);
}

@Test
public void testRemoveNonExistingDocument() {
    // 2 (we do not expect anything)
    replay(mock); // 3
    classUnderTest.removeDocument("Does not exist");
}
При активации в шаге 3, mock - это Mock-объект для интерфейса Collaborator, который не ожидает никаких вызовов. Это значит, что если изменить наш ClassUnderTest таким образом, чтобы он вызывал методы интерфейса, то Mock-объект сгенерирует исключение AssertionError:
java.lang.AssertionError:
  Unexpected method call documentRemoved("Does not exist"):
    at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:29)
    at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:44)
    at $Proxy0.documentRemoved(Unknown Source)
    at org.easymock.samples.ClassUnderTest.notifyListenersDocumentRemoved(ClassUnderTest.java:74)
    at org.easymock.samples.ClassUnderTest.removeDocument(ClassUnderTest.java:33)
    at org.easymock.samples.ExampleTest.testRemoveNonExistingDocument(ExampleTest.java:24)
    ...
Добавим поведение
Напишем второй тест. При вызове добавления документа в экземпляре тестируемого класса, мы ожидаем вызов mock.documentAdded() с передачей имени документа в качестве аргумента.
@Test
public void testAddDocument() {
    mock.documentAdded("New Document"); // 2
    replay(mock); // 3
    classUnderTest.addDocument("New Document", new byte[0]);
}
Таким образом, когда мы в состоянии "записи" (перед вызовом replay) Mock-объект не ведет себя как Mock-объект, а только записывает вызовы методов. После вызова replay, он ведет себя как Mock-объект, который будет проверять действительно ли методы были вызваны.

Если classUnderTest.addDocument("New Document", new byte[0]) вызовет метод с неправильным аргументом, будет сгенерировано исключение AssertionError:
java.lang.AssertionError:
  Unexpected method call documentAdded("Wrong title"):
    documentAdded("New Document"): expected: 1, actual: 0
    at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:29)
    at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:44)
    at $Proxy0.documentAdded(Unknown Source)
    at org.easymock.samples.ClassUnderTest.notifyListenersDocumentAdded(ClassUnderTest.java:61)
    at org.easymock.samples.ClassUnderTest.addDocument(ClassUnderTest.java:28)
    at org.easymock.samples.ExampleTest.testAddDocument(ExampleTest.java:30)
    ...
Если метод вызвается слишком часто, мы так же получим исключение:
java.lang.AssertionError:
  Unexpected method call documentAdded("New Document"):
    documentAdded("New Document"): expected: 1, actual: 2
    at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:29)
    at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:44)
    at $Proxy0.documentAdded(Unknown Source)
    at org.easymock.samples.ClassUnderTest.notifyListenersDocumentAdded(ClassUnderTest.java:62)
    at org.easymock.samples.ClassUnderTest.addDocument(ClassUnderTest.java:29)
    at org.easymock.samples.ExampleTest.testAddDocument(ExampleTest.java:30)
    ...
Проверка
Чтобы проверить действительно ли вызывался метод Mock-объекта вы можете вызвать verify(mock):
@Test
public void testAddDocument() {
    mock.documentAdded("New Document"); // 2
    replay(mock); // 3
    classUnderTest.addDocument("New Document", new byte[0]);
    verify(mock);
}
Если метод не был вызван - сгенерирует исключение:
java.lang.AssertionError:
  Expectation failure on verify:
    documentAdded("New Document"): expected: 1, actual: 0
    at org.easymock.internal.MocksControl.verify(MocksControl.java:70)
    at org.easymock.EasyMock.verify(EasyMock.java:536)
    at org.easymock.samples.ExampleTest.testAddDocument(ExampleTest.java:31)
    ...
Несколько вызовов
До текущего момента мы ожидали одиночный вызов метода интерфейса. Что, если мы ожидаем несколько вызовов? Допустим добавление уже существуюего метода приводит к вызову mock.documentChanged(). Напишем для этого тест:
@Test
public void testAddAndChangeDocument() {
    mock.documentAdded("Document");
    mock.documentChanged("Document");
    mock.documentChanged("Document");
    mock.documentChanged("Document");
    replay(mock);
    classUnderTest.addDocument("Document", new byte[0]);
    classUnderTest.addDocument("Document", new byte[0]);
    classUnderTest.addDocument("Document", new byte[0]);
    classUnderTest.addDocument("Document", new byte[0]);
    verify(mock);
}
Чтобы избежать дублирования строк, мы могли бы воспользоваться методом times(int times) на объекте, возвращаемом expectLastCall(). Например:
@Test
public void testAddAndChangeDocument() {
    mock.documentAdded("Document");
    mock.documentChanged("Document");
    expectLastCall().times(3);
    replay(mock);
    classUnderTest.addDocument("Document", new byte[0]);
    classUnderTest.addDocument("Document", new byte[0]);
    classUnderTest.addDocument("Document", new byte[0]);
    classUnderTest.addDocument("Document", new byte[0]);
    verify(mock);
}
Указываем возвращаемое значение
Для того, чтобы указать возвращаемое значение мы оборачиваем вызов в expect(T value) и указываем возврщаемое значение с помощью andReturn(Object returnValue).

Например, мы хотим проверить удаление документа. При вызове метода удаления ClassUnderTest запрашивает всех своих слушателей о разрешении удалить документ путем вызова voteForRemoval(String title). Если все слушатели согласны с удалением - документ удаляется и всем слушателям посылается documentRemoved(String title).
@Test
public void testVoteForRemoval() {
    mock.documentAdded("Document");   // expect document addition
    // expect to be asked to vote for document removal, and vote for it
    expect(mock.voteForRemoval("Document")).andReturn((byte) 42);
    mock.documentRemoved("Document"); // expect document removal
    replay(mock);
    classUnderTest.addDocument("Document", new byte[0]);
    assertTrue(classUnderTest.removeDocument("Document"));
    verify(mock);
}

@Test
public void testVoteAgainstRemoval() {
    mock.documentAdded("Document");   // expect document addition
    // expect to be asked to vote for document removal, and vote against it
    expect(mock.voteForRemoval("Document")).andReturn((byte) -42);
    replay(mock);
    classUnderTest.addDocument("Document", new byte[0]);
    assertFalse(classUnderTest.removeDocument("Document"));
    verify(mock);
}
Тип возвращемого значения определяется на этапе компиляции. Например, следующий код не скомпилируется, т.к. тип возвращаемого значения указан неверно:
expect(mock.voteForRemoval("Document")).andReturn("wrong type");
Так же вместо
expect(mock.voteForRemoval("Document")).andReturn((byte) 42);
мы могли бы использовать expectLastCall(). Например:
mock.voteForRemoval("Document");
expectLastCall().andReturn((byte) 42);
Работа с исключениями
Объекты, возвращаемые через expectLastCall() и expect(T value) предоставляют метод andThrow(Throwable throwable). Его использование аналогично методу andReurn().

Конечно, это далеко не всё, что умеет EasyMock и далеко не все, что вы хотели бы применить в реальной работе, но думаю, что для обзора достаточно. За более подробной информацией - обращайтесь к документации EasyMock, благо она очень даже толковая.

Комментариев нет:

Отправить комментарий

Примечание. Отправлять комментарии могут только участники этого блога.