Моки, стабы и шпионы в Spock Framework

в 8:09, , рубрики: groovy, Groovy & Grails, java, mock, spock, stub, tdd, Блог компании Haulmont

Spock предоставляет 3 мощных (но разных по сути) инструмента, упрощающих написание тестов: Mock, Stub и Spy.

Моки, стабы и шпионы в Spock Framework - 1

Довольно часто коду, который нужно протестировать, требуется взаимодействовать с внешними модулями, называющимися зависимостями (в оригинальной статье используется термин collaborators, который не очень распространён в русскоязычной среде).

Модульные тесты чаще всего разрабатываются для тестирования одного изолированного класса при помощи различных вариантов моков: Mock, Stub и Spy. Так тесты будут надёжнее и будут реже ломаться по мере того, как код зависимостей эволюционирует.

Такие изолированные тесты менее подвержены проблемам при изменении внутренних деталей реализации зависимостей.

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

TL;DR

Mocks

Используйте Mock для:

  • проверки контракта между тестируемым кодом и зависимостями
  • проверки того, что методы зависимостей вызываются корректное число раз
  • проверки корректности параметров, с которыми вызывается код зависимостей

Stubs

Используйте Stub для:

  • предоставления предопределённых результатов вызовов
  • выполнения предопределённых действий, ожидаемых от зависимостей, таких как выбрасывание исключений

Spies

Бойтесь шпионов (Spy). Как сказано в документации Spock:

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

Но так уж случается, что бывают ситуации, когда мы должны работать с легаси кодом. Легаси код бывает сложно или даже невозможно протестировать при помощи моков и стабов. В этом случае есть всего один вариант решения — использовать Spy.

Лучше иметь легаси код, покрытый тестами с использованием Spy, чем не иметь тестов для легаси совсем.

Используйте Spy для:

  • тестирования легаси кода, который невозможно протестировать другими методами
  • проверки того, что методы зависимостей вызываются корректное число раз
  • проверки корректности передаваемых параметров
  • предоставления предопределённого ответа от зависимостей
  • выполнения предопределённых действий в ответ на вызовы методов зависимостей

Mocks

Моки, стабы и шпионы в Spock Framework - 2

Вся сила моков проявляется, когда задача модульного теста состоит в проверке контракта между тестируемым кодом и зависимостями. Давайте посмотрим на следующий пример, где у нас имеется контроллер FooController, который использует FooService в качестве зависимости, и протестируем эту функциональность при помощи моков.

FooController.groovy

package com.mycompany.myapp

import groovy.transform.CompileStatic

@CompileStatic
class FooController {
    FooService fooService

    def doSomething() {
        render fooService.doSomething("Sally")
    }
}

FooService.groovy

package com.mycompany.myapp

import groovy.transform.CompileStatic

@CompileStatic
class FooService {

    String doSomething(String name) {
        "Hi ${name}, FooService did something"
    }
}

В этом сценарии мы хотим написать тест, который проверит:

  • контракт между FooController и FooService
  • FooService.doSomething(name) вызывается корректное число раз
  • FooService.doSomething(name) вызывается с корректным параметром

Взглянем на тест:

MockSpec.groovy

package com.mycompany.myapp

import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification

class MockSpec extends Specification implements ControllerUnitTest<FooController> {

    void "Mock FooService"() {
        given: "создаём мок зависимости"
        def fooService = Mock(FooService)

        and: "устанавливаем экземпляр мока в контроллер"
        controller.fooService = fooService

        when: "вызываем действие контроллера"
        controller.doSomething()

        then: "мок можно использовать для проверки числа вызовов и значений параметров"
        1 * fooService.doSomething("Sally")

        and: "мок возвращает 'пустое' значение по умолчанию - 'null'"
        response.text == null.toString()
    }
}

Приведённый тест создаёт мок сервиса:

def fooService = Mock(FooService)

Также тест проверяет, что FooService.doSomething(name) вызывается один раз, и параметр, переданный в него, совпадает со строкой "Sally".

1 * fooService.doSomething("Sally")

Приведённый код решает 4 важные задачи:

  • создаёт мок для FooService
  • убеждается в том, что FooService.doSomething(String name) вызывается ровно один раз с параметром String и значением "Sally"
  • изолирует тестируемый код, заменяя реализацию зависимости

Stubs

Использует ли тестируемый код зависимости? Является ли целью тестирования удостовериться, что тестируемый код работает корректно при взаимодействии с зависимостями? Являются ли результаты вызовов методов зависимостей входными значениями для тестируемого кода?

Если поведение тестируемого кода изменяется в зависимости от поведения зависимостей, то вам необходимо использовать стабы (Stub).

Давайте посмотрим на следующий пример с FooController и FooService и протестируем функциональность контроллера при помощи стабов.

FooController.groovy

package com.mycompany.myapp

import groovy.transform.CompileStatic

@CompileStatic
class FooController {
    FooService fooService

    def doSomething() {
        render fooService.doSomething("Sally")
    }
}

FooService.groovy

package com.mycompany.myapp

import groovy.transform.CompileStatic

@CompileStatic
class FooService {

    String doSomething(String name) {
        "Hi ${name}, FooService did something"
    }
}

Код теста:

StubSpec.groovy

package com.mycompany.myapp

import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification

class StubSpec extends Specification implements ControllerUnitTest<FooController> {

    void "Stub FooService"() {
        given: "создаём стаб сервиса"
        def fooService = Stub(FooService) {
            doSomething(_) >> "Stub did something"
        }

        and: "устанавливаем экземпляр стаба в контроллер"
        controller.fooService = fooService

        when: "вызываем действие контроллера"
        controller.doSomething()

        then: "стаб возвращает константное значение"
        // 1 * fooService.doSomething()
        // проверки числа вызовов не поддерживаются для стабов 
        response.text == "Stub did something"
    }
}

Создать стаб можно так:

def fooService = Stub(FooService) {
    doSomething(_) >> "Stub did something"
}

Приведённый код решает 4 важные задачи:

  • создаёт стаб FooService
  • убеждается в том, что FooService.doSomething(String name) вернёт строку "Stub did something" независимо от переданного параметра (поэтому мы использовали символ _)
  • изолирует тестируемый код, заменяя реализацию зависимости на стаб

Spies

Пожалуйста не читайте этот раздел.

Не смотрите.

Пропускайте и переходите к следующему.

Всё ещё читаете? Ну что ж, хорошо, давайте разбираться со Spy.

Моки, стабы и шпионы в Spock Framework - 3

Не используйте Spy. Как сказано в документации Spock:

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

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

Шпионы отличаются от моков или стабов, потому что они работают не как заглушки.

Когда зависимость подменяется моком или стабом, создается тестовый объект, а настоящий исходный код зависимости не выполняется.

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

Давайте посмотрим на следующий пример FooController, который использует FooService, а затем протестируем функциональность с помощью шпиона.

FooController.groovy

package com.mycompany.myapp

import groovy.transform.CompileStatic

@CompileStatic
class FooController {
    FooService fooService

    def doSomething() {
        render fooService.doSomething("Sally")
    }
}

FooService.groovy

package com.mycompany.myapp

import groovy.transform.CompileStatic

@CompileStatic
class FooService {
    String doSomething(String name) {
        "Hi ${name}, FooService did something"
    }
}

Код теста:

SpySpec.groovy

package com.mycompany.myapp

import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification

class SpySpec extends Specification implements ControllerUnitTest<FooController> {

    void "Spy FooService"() { 
        given: "создаём экземпляр-шпион"
        def fooService = Spy(FooService)

        and: "устанавливаем зависимость в контроллер"
        controller.fooService = fooService

        when: "вызываем действие контроллера"
        controller.doSomething()

        then: "проверяем число вызовов и значения параметров"
        1 * fooService.doSomething("Sally") >> "A Spy can modify implementation"

        and: 'шпион может изменять реализацию методов зависимостей'
        response.text == "A Spy can modify implementation"
    }
}

Создать экземпляр-шпион довольно просто:

def fooService = Spy(FooService)

В приведённом коде шпион позволяет нам проверить вызов FooService.doSomething(name), количество вызовов и значения параметров. Более того, шпион изменяет реализацию метода, чтобы вернуть другое значение.

1 * fooService.doSomething("Sally") >> "A Spy can modify implementation"

Приведённый код решает 4 важные задачи:

  • создаёт экземпляр шпион для FooService
  • проверяет взаимодействие с зависимостями
  • проверяет, как приложение работает в соответствии с определёнными результатами вызовов методов зависимостей
  • изолирует тестируемый код, заменяя реализацию зависимости на стаб

FAQ

Какой из вариантов использовать: Mock, Stub или Spy?

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

Q: Является ли целью тестирования проверка контракта между тестируемым кодом и зависимостями?

A: Если вы ответили Да, используйте Mock

Q: Является ли целью тестирования удостовериться, что тестируемый код работает верно при взаимодействии с зависимостями?

A: Если вы ответили Да, используйте Stub

Q: Являются ли результаты вызовов методов зависимостей входными значениями для тестируемого кода?

A: Если вы ответили Да, используйте Stub

Q: Работаете ли вы с легаси кодом, который очень сложно протестировать, и у вас не осталось вариантов?

A: Попробуйте использовать Spy

Код примеров

Вы можете найти код всех примеров этой статьи по ссылке:

https://github.com/ddelponte/mock-stub-spy

Полезные ссылки

Автор: jreznot

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js