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