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