Научитесь создавать и внедрять макеты, создавая ожидания и проверки с помощью библиотеки JMockit в тестах JUnit. Мы начнем с базовых концепций JMockit с примером, а затем углубимся в продвинутые концепции.
1. Основные концепции JMockit
1.1 Основные характеристики
JMockit — это программное обеспечение с открытым исходным кодом, которое содержит поддержку для имитации, фиктивного и интеграционного тестирования, а также инструмент покрытия кода. Он используется для имитации внешних зависимостей за пределами границ теста, подобно Mockito и другим подобным библиотекам имитации.
Самая важная особенность JMockit заключается в том, что он позволяет нам делать моки чего угодно, даже то, что трудно сделать моки с помощью других библиотек, например, закрытые методы, конструкторы, статические и финальные методы. Он даже позволяет делать моки полей-членов и блоков инициализации.
1.2 Этапы испытаний
Подобно EasyMock, JMockit также использует модель «Запись-Воспроизведение-Проверка» в тесте после определения макетов и SUT(тестируемой системы).
- Запись: На этом этапе мы записываем ожидания от фиктивных объектов. Мы определяем поведение фиктивных объектов, т.е. вызываемый метод, возвращаемое значение и ожидаемое количество вызовов.
- Повтор: На этом этапе мы выполняем реальный тестовый код, написанный в SUT(тестируемая система).
- Проверка: На этом этапе мы проверяем, были ли выполнены все ожидания.
Типичный тест JMockit будет выглядеть так:
public class TestClass {@Testedprivate Service service;@Injectableprivate Dao dao;@Mockprivate Component component;@Testpublic void testSUT() {// Test data initialization, if anynew Expectations() {{// define expected behaviour for mocks and injectables}};// test service operationsnew Verifications() {{// verify mocks and injectables}};// assertions}}
1.3. Декларативные ожидания и проверки
JMockit позволяет определять ожидания и проверки очень подробно и декларативно. Их очень легко отличить от остального тестового кода.
Другие библиотеки имитации, как правило, предоставляют статические методы, такие как expect(), andThenReturn() и times(), для указания ожиданий, а также verify() для проверки ожиданий после выполнения теста.
MockAPI.expect(mock.method(argumentMatcher)).andThenReturn(value).times(1);
Напротив, JMockit выражает их с помощью следующих классов:
- Ожидания: Блок ожиданий представляет собой набор вызовов определенного имитированного метода/конструктора, который имеет значение для данного теста.
- Проверки: Обычный неупорядоченный блок для проверки того, что во время воспроизведения произошел хотя бы один соответствующий вызов.
- VerificationsInOrder: его следует использовать, когда мы хотим проверить фактический относительный порядок вызовов во время фазы воспроизведения.
- FullVerfications: Если мы хотим, чтобы все вызовы фиктивных типов/экземпляров, участвующих в тесте, были проверены. Это гарантирует, что ни один вызов не останется непроверенным.
Мы еще вернемся к этим классам позже в этом уроке.
2. Простой пример теста JMockit
2.1 Зависимость Maven
Начните с включения зависимости JMockit в приложение. Если вы еще не включили, добавьте также зависимости JUnit.
<dependency><groupId>org.jmockit</groupId><artifactId>jmockit</artifactId><version>1.49</version></dependency>
2.2 Тестируемая система
Для демонстрации синтаксиса JMockit мы создали типичный пример использования, в котором RecordService вызывает RecordDao для сохранения записи и отправки уведомления с помощью NotificationService. RecordService использует класс SequenceGenerator для получения идентификатора следующей записи.
Вы можете просмотреть код в репозитории GitHub, ссылка на который находится в конце этого руководства.
2.3. Тестовая демонстрация
Чтобы протестировать метод RecordService.saveRecord(), нам нужно внедрить RecordDao и SequenceGenerator в качестве зависимостей. RecordService получает экземпляр NotificationService во время выполнения, поэтому мы можем просто имитировать его и позволить времени выполнения заменить его имитатором.
Далее мы создадим некоторые Ожидания, выполним тестовый код и, наконец, выполним Проверки для завершения теста. Мы можем использовать дополнительные утверждения JUnit для проверки дополнительных результатов теста.
public class JMockitDemoTests {@InjectableRecordDao mockDao; // Dependency@InjectableSequenceGenerator mockGenerator; // Dependency@TestedRecordService service; //System Under Test// NotificationService can be mocked in test scope@Testpublic void testSaveRecord(@Mocked NotificationService notificationService) {Record record = new Record();record.setName("Test Record");//Register Expectationsnew Expectations() {{mockGenerator.getNext();result = 100L;times = 1;}};new Expectations() {{mockDao.saveRecord(record);result = record;times = 1;}};new Expectations() {{notificationService.sendNotification(anyString);result = true;times = 1;}};//Test codeRecord savedRecord = service.saveRecord(record);// Verificationsnew Verifications() {{ // a "verification block"mockGenerator.getNext();times = 1;}};new Verifications() {{mockDao.saveRecord(record);times = 1;}};new Verifications() {{notificationService.sendNotification(anyString);times = 1;}};//Additional assertionsassertEquals("Test Record", savedRecord.getName());assertEquals(100L, savedRecord.getId());}}
3. Создание и внедрение макетов
Стоит помнить, что JMockit позволяет иметь разные фиктивные объекты на разных фазах потока запись-воспроизведение-проверка. Например, мы можем иметь два фиктивных объекта для зависимости и использовать их отдельно в ожиданиях и проверках.
В отличие от других API-интерфейсов имитации, эти имитированные объекты не обязательно должны быть теми, которые используются тестируемым кодом, когда он вызывает методы экземпляра для своих зависимостей.
@Mocked Dependency mockDependency;@Testpublic void testCase(@Mocked Dependency anotherMockDependency){new Expectations() {{mockDependency.operation();}};// Call the code under testnew Verifications() {{anotherMockDependency.operation();}};}
JMockit позволяет создавать и внедрять мок-объекты для SUT разными способами. Давайте узнаем о них.
3.1 Аннотации JMockit
Основные аннотации для имитации зависимостей следующие.
3.1.1. @Mocked и @Capturing
При использовании в поле @Mocked создаст имитированные экземпляры каждого нового объекта этого конкретного класса во время выполнения теста. Внутри он имитирует все методы и конструкторы во всех экземплярах имитированного класса.
@Mocked Dependency mockDependency;
@Capturing ведет себя аналогично @Mocked, но дополнительно @Capturing имитирует каждый подкласс, расширяющий или реализующий тип аннотированного поля.
В следующем примере JMockit будет имитировать все экземпляры Dependency, а также любые его подклассы. Если Dependency является интерфейсом, то JMockit будет имитировать все его реализующие классы.
@Capturing Dependency mockDependency;
Обратите внимание, что фиктивные поля, аннотированные только @Mocked или @Capturing, не рассматриваются для внедрения.
3.1.2. @Инъекционный и @тестируемый
Аннотация @Tested запускает автоматическое создание экземпляров и инъекцию других фиктивных и инъекционных объектов непосредственно перед выполнением тестового метода. Экземпляр будет создан с использованием подходящего конструктора тестируемого класса, при этом гарантируя, что его внутренние зависимости @Injectable будут правильно внедрены(когда применимо).
В отличие от @Mocked и @Capturing, @Injectable создает только один имитированный экземпляр.
Обратите внимание, что при инициализации тестируемых классов JMockit поддерживает две формы внедрения: внедрение через конструктор и внедрение через поле.
В следующем примере dep1 и dep2 будут внедрены в SUT.
public class TestClass {@Tested SUT tested;@Injectable Dependency dep1;@Injectable AnotherDependency dep2;}
3.2 Тестовые классы и методы области действия Mocks
JMockit позволяет создавать макеты на уровне класса, а также на уровне метода теста, передавая макеты в качестве параметров теста. Макеты на уровне метода помогают создать макет только для одного теста и, таким образом, помогают еще больше ограничить границы теста.
public class TestClass {//Class scoped mock@Mocked Dependency mock;//Method scoped mock@Testpublic void testCase(@Mocked AnotherDependency anotherMock){//test code}}
4. Запись ожиданий
4.1 Соответствие вызовов методов
JMockit очень гибок в записи ожиданий. Мы можем записывать несколько вызовов методов в одном блоке Expectations, а также, и мы можем записывать несколько блоков Expectations в одном тестовом методе.
public TestClass {new Expectations() {{mock.method1();mock.method2();anotherMock.method3();}};new Expectations() {{someOtherMock.method();}};}
4.2. Сопоставление аргументов
Использование точных аргументов в вызовах методов будет соответствовать точным значениям аргументов в фазе воспроизведения. Аргументы объектного типа проверяются на равенство с помощью метода equals(). Аналогично аргументы типа массивов и списков рассматриваются как равные, если оба массива или списка имеют одинаковый размер и содержат похожие элементы.
Для гибкого сопоставления аргументов мы можем использовать один из следующих двух подходов:
4.2.1. любые поля
JMockit предоставляет ряд любых полей сопоставления аргументов. Они поддерживают одно для каждого примитивного типа(и соответствующего класса-обертки), одно для строк и одно для всех объектов.
new Expectations() {{mock.method1(anyInt);mock.method2(anyString);mock.method3(anyInt);mock.method4((List<?>) any);mockDao.saveRecord((Record) any);}};
4.2.2. с методами
Мы можем использовать метод withXYZ() из ряда таких методов для конкретных применений. Эти методы — withEqual(), withNotEqual(), withNull(), withNotNull(), withSubstring(), withPrefix(), withSuffix(), withMatch(regex), withSameInstance(), withInstanceLike() и withInstanceOf() и т. д.
new Expectations() {{mock.method1(withSubstring("xyz"));mock.method2(withSameInstance(record));mock.method3(withAny(1L)); //Any long value will matchmock.method4((List<?>) withNotNull());}};
4.3 Сопоставление возвращаемых значений
Если непустые фиктивные методы, мы можем записать возвращаемые значения в поле результата. Присвоение результату должно появиться сразу после вызова, который идентифицирует записанное ожидание.
new Expectations() {{mock.method1();result = value1;mock.method2();result = value2;}};
Если мы вызываем метод в цикле, то мы можем ожидать несколько возвращаемых значений, либо используя метод returns(v1, v2, …), либо назначая список значений полю результата.
new Expectations() {{mock.method();returns(value1, value2, value3);}};
Если вместо этого тесту необходимо выдать исключение или ошибку при вызове метода, просто присвойте желаемый экземпляр throwable результату.
new Expectations() {{mock.method();result = new ApplicationException();}};
4.3 Соответствующее количество вызовов
JMockit предоставляет три специальных поля, просто соответствующих количеству вызовов. Любые вызовы меньше или больше ожидаемого нижнего или верхнего предела, соответственно, и выполнение теста автоматически завершится неудачей.
- раз
- minTimes
- maxTimes
new Expectations() {{mock.method();result = value;times = 1;}};
5. Написание подтверждений
5.1 Проверки
Внутри блоков Verifications мы можем использовать те же шаги, которые доступны в блоках Expectations, за исключением возвращаемых значений и выданных исключений. Мы можем повторно использовать вызовы методов и подсчет из ожиданий.
Итак, синтаксис для написания проверок такой же, как и в ожиданиях, и вы можете обратиться к предыдущим разделам для получения информации.
new Verifications() {{mock.method();times = 1;}};
5.2. ПроверкиВПорядке
Как упоминалось в разделе 1.3, это помогает проверить фактический относительный порядок вызовов во время фазы воспроизведения. Внутри этого блока просто запишите вызовы в один или несколько макетов в том порядке, в котором они, как ожидается, должны были произойти.
@Testpublic void testCase() {//Expectation//Test codemock.firstInvokeThis();mock.thenInvokeThis();mock.finallyInvokeThis();//Verificationnew VerificationsInOrder() {{mock.firstInvokeThis();mock.thenInvokeThis();mock.finallyInvokeThis();}};}
5.3. Полные проверки
В предыдущих режимах проверки JMockit проверяет, что все вызовы в блоке Verifications должны быть выполнены хотя бы один раз во время фазы воспроизведения теста. Он не жалуется на те вызовы, которые произошли в фазе воспроизведения, но не были добавлены в блок Verifications.
В следующем примере method3() был выполнен в тесте, но не проверен на этапе проверки. Тест будет ПРОЙДЕН.
@Тестpublic void testCase() {//Тестовый кодфиктивный.метод1();фиктивный.метод2();фиктивный метод3();//Проверкановый ПроверкиВПорядке() {{фиктивный.метод1();фиктивный.метод2();}};}
Если мы хотим взять под полный контроль фиктивные взаимодействия, то мы можем использовать FullVerifications. Это помогает предотвратить выполнение любого метода, который мы не проверяем.
@Тестpublic void testCase() {//Тестовый кодфиктивный.метод1();фиктивный.метод2();фиктивный метод3();//Проверкановые ПолныеПроверки() {{фиктивный.метод1();фиктивный.метод2();mock.method3(); //Если мы удалим это, тест НЕ будет пройден}};}
6. Заключение
В этом уроке мы подробно изучили использование функциональности мокинга, предоставляемой JMockit. Мы подробно изучили фазы запись-воспроизведение-проверка и примеры.
Мы также изучили такие продвинутые концепции, как гибкое сопоставление аргументов и количество вызовов.