«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(), используя заглушку для зависимости 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')]

«

Мы используем операнд \u003e\u003e для заглушки метода. Метод 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. Код с взаимодействием

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

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

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

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. Проверка взаимодействия с фиктивными объектами

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

class ItemServiceTest extends Specification {

    ItemProvider itemProvider
    ItemService itemService
    EventPublisher eventPublisher

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

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

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')
}

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

1 * eventPublisher.publish('a')

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

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

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

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

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

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

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')]

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

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

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

Итак, в общем, эта строка говорит: itemProvider.getItems будет вызван один раз с аргументом [‘item-‘id’] и вернет заданный массив.

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

6. Шпионские классы в Spock

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

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

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

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

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

6.2. Тестирование с помощью Spy

eventPublisher = Spy(LoggingEventPublisher)

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

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

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

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

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

I've published: item-id

«

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

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

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

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

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

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