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

Original author: Dean Del Ponte
  • Translation

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



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


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


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


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


TL;DR


Mocks


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


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

Stubs


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


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

Spies


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


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

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


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


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


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

Mocks



Вся сила моков проявляется, когда задача модульного теста состоит в проверке контракта между тестируемым кодом и зависимостями. Давайте посмотрим на следующий пример, где у нас имеется контроллер 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.



Не используйте 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


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


  • +12
  • 2.7k
  • 3
Haulmont
244.66
Создаем современные корпоративные системы
Share post

Similar posts

Comments 3

    0
    Автор, оформите блоки кода правильно, невозможно читать
      +1
      Тысяча извинений, забыл ткнуть в кнопку выбора Markdown. Fixed
      +1
      Стоит заметить, что mocks могут применяться как для проверки вызова, так и для возвращения результата метода. В то время stubs только для возвращения результата.
      Согласно вашей заметке, для возвращения результата нужно всегда использовать stubs, а не mock. Что не совсем корректно.
      Whereas a mock can be used both for stubbing and mocking, a stub can only be used for stubbing.

      Ссылка на документацию

      Only users with full accounts can post comments. Log in, please.