Мы можем писать такие классы-заглушки сами, а можем воспользоваться одним из 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-объектом нужно:
- Создать экземпляр Mock-объекта для тестируемого интерфейса
- Указать ожидаемое поведение
- Переключить 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, благо она очень даже толковая.
Комментариев нет:
Отправить комментарий
Примечание. Отправлять комментарии могут только участники этого блога.