Перейти к основному содержимому

Разница между Stub, Mock и Spy в Spock Framework

· 9 мин. чтения

1. Обзор

В этом уроке мы собираемся обсудить различия между Mock , Stub и Spy в среде Spock . Мы проиллюстрируем, что предлагает инфраструктура в отношении тестирования на основе взаимодействия.

Spock — это среда тестирования для Java и Groovy , которая помогает автоматизировать процесс ручного тестирования программного приложения. Он представляет свои собственные макеты, заглушки и шпионы и поставляется со встроенными возможностями для тестов, которые обычно требуют дополнительных библиотек.

Во-первых, мы проиллюстрируем, когда мы должны использовать заглушки. Затем мы пройдем через насмешки. В конце мы опишем недавно представленный Spy .

2. Зависимости Maven

Прежде чем мы начнем, давайте добавим наши зависимости Maven :

<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.3-RC1-groovy-2.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.7</version>
<scope>test</scope>
</dependency>

Обратите внимание, что нам понадобится версия Spock 1.3-RC1-groovy-2.5 . Spy будет представлен в следующей стабильной версии Spock Framework. Прямо сейчас Spy доступен в первом релиз-кандидате версии 1.3.

Для ознакомления с базовой структурой теста Spock ознакомьтесь с нашей вводной статьей о тестировании с помощью Groovy и Spock .

3. Тестирование на основе взаимодействия

Тестирование на основе взаимодействия — это метод, который помогает нам проверять поведение объектов , в частности, то, как они взаимодействуют друг с другом. Для этого мы можем использовать фиктивные реализации, называемые макетами и заглушками.

Конечно, мы могли бы очень легко написать собственные реализации моков и заглушек. Проблема возникает, когда объем нашего производственного кода растет. Писать и поддерживать этот код вручную становится сложно. Вот почему мы используем фиктивные фреймворки, которые дают краткий способ краткого описания ожидаемых взаимодействий. Spock имеет встроенную поддержку насмешек, заглушек и шпионажа.

Как и большинство библиотек Java, Spock использует динамический прокси-сервер JDK для имитации интерфейсов и прокси-серверы Byte Buddy или cglib для имитации классов. Он создает фиктивные реализации во время выполнения.

В Java уже есть много разных и зрелых библиотек для имитации классов и интерфейсов. Хотя каждый из них можно использовать в Spock , все же есть одна основная причина, по которой мы должны использовать имитаторы Spock, заглушки и шпионы. Внедряя все это в Spock, мы можем использовать все возможности Groovy, чтобы сделать наши тесты более читабельными, простыми в написании и, безусловно, более увлекательными!

4. Заглушки вызовов методов

Иногда в модульных тестах нам нужно обеспечить фиктивное поведение класса . Это может быть клиент для внешней службы или класс, предоставляющий доступ к базе данных. Эта техника известна как стаббинг.

Заглушка — это управляемая замена существующей зависимости класса в нашем протестированном коде. Это полезно для выполнения вызова метода, который отвечает определенным образом. Когда мы используем заглушку, нам все равно, сколько раз будет вызываться метод. Вместо этого мы просто хотим сказать: возвращайте это значение при вызове с этими данными.

Перейдем к примеру кода с бизнес-логикой.

4.1. Тестируемый код

Давайте создадим класс модели с именем Item :

public class Item {
private final String id;
private final String name;

// standard constructor, getters, equals
}

Нам нужно переопределить метод equals(Object other) , чтобы наши утверждения работали. Spock будет использовать equals во время утверждений, когда мы используем двойной знак равенства (==):

new Item('1', 'name') == new Item('1', 'name')

Теперь давайте создадим интерфейс ItemProvider с одним методом:

public interface ItemProvider {
List<Item> getItems(List<String> itemIds);
}

Нам также понадобится класс, который будет протестирован. Мы добавим ItemProvider в качестве зависимости в ItemService:

public class ItemService {
private final ItemProvider itemProvider;

public ItemService(ItemProvider itemProvider) {
this.itemProvider = itemProvider;
}

List<Item> getAllItemsSortedByName(List<String> itemIds) {
List<Item> items = itemProvider.getItems(itemIds);
return items.stream()
.sorted(Comparator.comparing(Item::getName))
.collect(Collectors.toList());
}

}

Мы хотим, чтобы наш код зависел от абстракции, а не от конкретной реализации. Вот почему мы используем интерфейс. Это может иметь множество различных реализаций. Например, мы можем прочитать элементы из файла, создать HTTP-клиент для внешней службы или прочитать данные из базы данных.

В этом коде нам нужно заглушить внешнюю зависимость, потому что мы хотим протестировать только нашу логику, содержащуюся в методе getAllItemsSortedByName .

4.2. Использование объекта-заглушки в тестируемом коде

Давайте инициализируем объект ItemService в методе setup() , используя Stub для зависимости ItemProvider :

ItemProvider itemProvider
ItemService itemService

def setup() {
itemProvider = Stub(ItemProvider)
itemService = new ItemService(itemProvider)
}

Теперь давайте заставим itemProvider возвращать список элементов при каждом вызове с определенным аргументом :

itemProvider.getItems(['offer-id', 'offer-id-2']) >> 
[new Item('offer-id-2', 'Zname'), new Item('offer-id', 'Aname')]

Мы используем операнд >> для заглушки метода. Метод getItems всегда будет возвращать список из двух элементов при вызове со списком ['offer-id', 'offer-id-2'] . [] — это ярлык Groovy для создания списков.

Вот весь метод тестирования:

def 'should return items sorted by name'() {
given:
def ids = ['offer-id', 'offer-id-2']
itemProvider.getItems(ids) >> [new Item('offer-id-2', 'Zname'), new Item('offer-id', 'Aname')]

when:
List<Item> items = itemService.getAllItemsSortedByName(ids)

then:
items.collect { it.name } == ['Aname', 'Zname']
}

Мы можем использовать гораздо больше возможностей заглушек, таких как: использование ограничений сопоставления аргументов, использование последовательностей значений в заглушках, определение различного поведения в определенных условиях и связывание ответов методов.

5. Насмешливые методы класса

Теперь давайте поговорим о фиктивных классах или интерфейсах в Spock.

Иногда мы хотели бы знать, был ли вызван какой-либо метод зависимого объекта с указанными аргументами . Мы хотим сосредоточиться на поведении объектов и исследовать, как они взаимодействуют, глядя на вызовы методов. ** ** Мокинг — это описание обязательного взаимодействия между объектами в тестовом классе.

Мы проверим взаимодействия в примере кода, описанном ниже.

5.1. Код с взаимодействием

В качестве простого примера мы собираемся сохранять элементы в базе данных. После успеха мы хотим опубликовать событие на брокере сообщений о новых элементах в нашей системе.

В качестве примера брокера сообщений используется RabbitMQ или Kafka , поэтому в целом мы просто опишем наш контракт:

public interface EventPublisher {
void publish(String addedOfferId);
}

Наш тестовый метод сохранит непустые элементы в базе данных, а затем опубликует событие. Сохранение элемента в базе данных в нашем примере неактуально, поэтому просто добавим комментарий:

void saveItems(List<String> itemIds) {
List<String> notEmptyOfferIds = itemIds.stream()
.filter(itemId -> !itemId.isEmpty())
.collect(Collectors.toList());

// save in database

notEmptyOfferIds.forEach(eventPublisher::publish);
}

5.2. Проверка взаимодействия с фиктивными объектами

Теперь давайте проверим взаимодействие в нашем коде.

Во- первых, нам нужно имитировать EventPublisher в нашем методе setup() . Таким образом, мы создаем новое поле экземпляра и имитируем его с помощью функции Mock(Class) :

class ItemServiceTest extends Specification {

ItemProvider itemProvider
ItemService itemService
EventPublisher eventPublisher

def setup() {
itemProvider = Stub(ItemProvider)
eventPublisher = Mock(EventPublisher)
itemService = new ItemService(itemProvider, eventPublisher)
}

Теперь мы можем написать наш тестовый метод. Мы передадим 3 строки: "", "a", "b" и ожидаем, что наш eventPublisher опубликует 2 события со строками "a" и "b":

def 'should publish events about new non-empty saved offers'() {
given:
def offerIds = ['', 'a', 'b']

when:
itemService.saveItems(offerIds)

then:
1 * eventPublisher.publish('a')
1 * eventPublisher.publish('b')
}

Давайте подробнее рассмотрим наше утверждение в последнем разделе then :

1 * eventPublisher.publish('a')

Мы ожидаем, что itemService вызовет eventPublisher.publish(String) с 'a' в качестве аргумента.

В заглушке мы говорили об ограничениях аргументов. Те же правила применяются и к макетам. Мы можем убедиться, что eventPublisher.publish(String) был вызван дважды с любым ненулевым и непустым аргументом:

2 * eventPublisher.publish({ it != null && !it.isEmpty() })

5.3. Сочетание насмешек и заглушек

В Spock Mock может вести себя так же, как Stub . Таким образом, мы можем сказать имитируемым объектам, что для данного вызова метода он должен возвращать данные данные.

Давайте переопределим ItemProvider с помощью Mock(Class) и создадим новый ItemService :

given:
itemProvider = Mock(ItemProvider)
itemProvider.getItems(['item-id']) >> [new Item('item-id', 'name')]
itemService = new ItemService(itemProvider, eventPublisher)

when:
def items = itemService.getAllItemsSortedByName(['item-id'])

then:
items == [new Item('item-id', 'name')]

Мы можем переписать заглушку из данного раздела:

1 * itemProvider.getItems(['item-id']) >> [new Item('item-id', 'name')]

Как правило, эта строка говорит: itemProvider.getItems будет вызываться один раз с аргументом ['item-'id'] и возвращать заданный массив .

Мы уже знаем, что макеты могут вести себя так же, как заглушки. Все правила, касающиеся ограничений аргументов, возврата нескольких значений и побочных эффектов, также применяются к Mock .

6. Шпионские занятия в Споке

Шпионы предоставляют возможность обернуть существующий объект. Это означает, что мы можем прослушивать разговор между вызывающим абонентом и реальным объектом, но сохраняем исходное поведение объекта. По сути, Spy делегирует вызовы методов исходному объекту .

В отличие от Mock and Stub , мы не можем создать Spy на интерфейсе. Он оборачивает фактический объект, поэтому дополнительно нам нужно будет передать аргументы для конструктора. В противном случае будет вызван конструктор типа по умолчанию.

6.1. Тестируемый код

Давайте создадим простую реализацию для EventPublisher. LoggingEventPublisher будет печатать в консоли идентификатор каждого добавленного элемента. Вот реализация метода интерфейса:

@Override
public void publish(String addedOfferId) {
System.out.println("I've published: " + addedOfferId);
}

6.2. Тестирование со шпионом

Мы создаем шпионов аналогично макетам и заглушкам, используя метод Spy(Class) . LoggingEventPublisher не имеет других зависимостей класса, поэтому нам не нужно передавать аргументы конструктора:

eventPublisher = Spy(LoggingEventPublisher)

Теперь давайте проверим нашего шпиона. Нам нужен новый экземпляр ItemService с нашим шпионским объектом:

given:
eventPublisher = Spy(LoggingEventPublisher)
itemService = new ItemService(itemProvider, eventPublisher)

when:
itemService.saveItems(['item-id'])

then:
1 * eventPublisher.publish('item-id')

Мы убедились, что метод eventPublisher.publish вызывался только один раз. Кроме того, вызов метода был передан реальному объекту, поэтому мы увидим вывод println в консоли:

I've published: item-id

Обратите внимание, что когда мы используем заглушку для метода Spy , он не будет вызывать метод реального объекта. Как правило, нам следует избегать использования шпионов. Если нам придется это сделать, может быть, нам следует изменить код в соответствии со спецификацией?

7. Хорошие модульные тесты

Давайте закончим кратким обзором того, как использование фиктивных объектов улучшает наши тесты:

  • мы создаем детерминированные наборы тестов
  • у нас не будет побочных эффектов
  • наши модульные тесты будут очень быстрыми
  • мы можем сосредоточиться на логике, содержащейся в одном классе Java
  • наши тесты не зависят от окружающей среды

8. Заключение

В этой статье мы подробно описали шпионов, mock-ы и заглушки в Groovy . Знания на эту тему сделают наши тесты быстрее, надежнее и читабельнее.

Реализацию всех наших примеров можно найти в проекте Github .