Разница между Mock / Stub / Spy в тестовой среде Spock


102

Я не понимаю разницы между Mock, Stub и Spy в тестировании Spock, и учебники, которые я просматривал в Интернете, не объясняют их подробно.

Ответы:


96

Внимание: в следующих параграфах я собираюсь излишне упростить и, возможно, даже немного фальсифицировать. Для получения более подробной информации см . Веб-сайт Мартина Фаулера .

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

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

Шпион - это своего рода гибрид между реальным объектом и заглушкой, то есть это в основном реальный объект с некоторыми (не всеми) методами, затененными методами-заглушками. Методы без заглушек просто перенаправляются к исходному объекту. Таким образом, вы можете иметь оригинальное поведение для «дешевых» или тривиальных методов и поддельное поведение для «дорогих» или сложных методов.


Обновление 2017-02-06: На самом деле ответ пользователя mikhail более специфичен для Спока, чем мой исходный ответ выше. Итак, в рамках Спока то, что он описывает, правильно, но это не искажает мой общий ответ:

  • Заглушка предназначена для имитации определенного поведения. В Spock это все, что может сделать заглушка, так что это самая простая вещь.
  • Имитация связана с заменой (возможно, дорогостоящим) реальным объектом, предоставляя ответы на все вызовы методов. В этом плане макет проще стаба. Но в Spock, mock может также заглушить результаты метода, то есть быть как mock, так и stub. Более того, в Spock мы можем подсчитать, как часто во время теста вызывались определенные имитационные методы с определенными параметрами.
  • Шпион всегда обертывает реальный объект и по умолчанию направляет все вызовы методов на исходный объект, также передавая исходные результаты. Подсчет вызовов методов также работает для шпионов. В Spock шпион также может изменять поведение исходного объекта, манипулируя параметрами вызова метода и / или результатами или блокируя вызов исходных методов вообще.

А теперь вот пример исполняемого теста, демонстрирующий, что возможно, а что нет. Это немного поучительнее михаиловских отрывков. Большое спасибо ему за то, что вдохновил меня улучшить свой ответ! :-)

package de.scrum_master.stackoverflow

import org.spockframework.mock.TooFewInvocationsError
import org.spockframework.runtime.InvalidSpecException
import spock.lang.FailsWith
import spock.lang.Specification

class MockStubSpyTest extends Specification {

  static class Publisher {
    List<Subscriber> subscribers = new ArrayList<>()

    void addSubscriber(Subscriber subscriber) {
      subscribers.add(subscriber)
    }

    void send(String message) {
      for (Subscriber subscriber : subscribers)
        subscriber.receive(message);
    }
  }

  static interface Subscriber {
    String receive(String message)
  }

  static class MySubscriber implements Subscriber {
    @Override
    String receive(String message) {
      if (message ==~ /[A-Za-z ]+/)
        return "ok"
      return "uh-oh"
    }
  }

  Subscriber realSubscriber1 = new MySubscriber()
  Subscriber realSubscriber2 = new MySubscriber()
  Publisher publisher = new Publisher(subscribers: [realSubscriber1, realSubscriber2])

  def "Real objects can be tested normally"() {
    expect:
    realSubscriber1.receive("Hello subscribers") == "ok"
    realSubscriber1.receive("Anyone there?") == "uh-oh"
  }

  @FailsWith(TooFewInvocationsError)
  def "Real objects cannot have interactions"() {
    when:
    publisher.send("Hello subscribers")
    publisher.send("Anyone there?")

    then:
    2 * realSubscriber1.receive(_)
  }

  def "Stubs can simulate behaviour"() {
    given:
    def stubSubscriber = Stub(Subscriber) {
      receive(_) >>> ["hey", "ho"]
    }

    expect:
    stubSubscriber.receive("Hello subscribers") == "hey"
    stubSubscriber.receive("Anyone there?") == "ho"
    stubSubscriber.receive("What else?") == "ho"
  }

  @FailsWith(InvalidSpecException)
  def "Stubs cannot have interactions"() {
    given: "stubbed subscriber registered with publisher"
    def stubSubscriber = Stub(Subscriber) {
      receive(_) >> "hey"
    }
    publisher.addSubscriber(stubSubscriber)

    when:
    publisher.send("Hello subscribers")
    publisher.send("Anyone there?")

    then:
    2 * stubSubscriber.receive(_)
  }

  def "Mocks can simulate behaviour and have interactions"() {
    given:
    def mockSubscriber = Mock(Subscriber) {
      3 * receive(_) >>> ["hey", "ho"]
    }
    publisher.addSubscriber(mockSubscriber)

    when:
    publisher.send("Hello subscribers")
    publisher.send("Anyone there?")

    then: "check interactions"
    1 * mockSubscriber.receive("Hello subscribers")
    1 * mockSubscriber.receive("Anyone there?")

    and: "check behaviour exactly 3 times"
    mockSubscriber.receive("foo") == "hey"
    mockSubscriber.receive("bar") == "ho"
    mockSubscriber.receive("zot") == "ho"
  }

  def "Spies can have interactions"() {
    given:
    def spySubscriber = Spy(MySubscriber)
    publisher.addSubscriber(spySubscriber)

    when:
    publisher.send("Hello subscribers")
    publisher.send("Anyone there?")

    then: "check interactions"
    1 * spySubscriber.receive("Hello subscribers")
    1 * spySubscriber.receive("Anyone there?")

    and: "check behaviour for real object (a spy is not a mock!)"
    spySubscriber.receive("Hello subscribers") == "ok"
    spySubscriber.receive("Anyone there?") == "uh-oh"
  }

  def "Spies can modify behaviour and have interactions"() {
    given:
    def spyPublisher = Spy(Publisher) {
      send(_) >> { String message -> callRealMethodWithArgs("#" + message) }
    }
    def mockSubscriber = Mock(MySubscriber)
    spyPublisher.addSubscriber(mockSubscriber)

    when:
    spyPublisher.send("Hello subscribers")
    spyPublisher.send("Anyone there?")

    then: "check interactions"
    1 * mockSubscriber.receive("#Hello subscribers")
    1 * mockSubscriber.receive("#Anyone there?")
  }
}

Здесь непонятна разница между макетом и заглушкой. С помощью моков нужно проверить поведение (будет ли и сколько раз вызываться метод). С помощью заглушек проверяется только состояние (например, размер коллекции после теста). К вашему сведению: моки тоже могут дать заранее подготовленные результаты.
chipiik 06

Спасибо @mikhail и chipiik за отзывы. Я обновил свой ответ, надеюсь, улучшив и прояснив некоторые вещи, которые я написал изначально. Отказ от ответственности: в моем первоначальном ответе я сказал, что чрезмерно упрощаю и слегка фальсифицирую некоторые факты, связанные со Споком. Я хотел, чтобы люди понимали основные различия между заглушкой, издевательством и шпионажем.
kriegaex

@chipiik, еще одна вещь в ответ на ваш комментарий: я много лет тренировал команды разработчиков и видел, как они используют Spock или другой JUnit с другими фреймворками. В большинстве случаев при использовании моков они делали это не для проверки поведения (то есть для подсчета вызовов методов), а для изоляции испытуемого объекта от его окружения. Подсчет взаимодействий IMO - это просто дополнительный бонус, и его следует использовать с осторожностью и осторожностью, потому что такие тесты имеют тенденцию ломаться, когда они проверяют проводку компонентов больше, чем их фактическое поведение.
kriegaex 06

Его краткий, но все же очень полезный ответ
Чакладер Асфак Арефе

55

Вопрос был в контексте структуры Spock, и я не верю, что текущие ответы учитывают это.

На основе документации Spock (примеры настроены, добавлены мои собственные формулировки):

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

subscriber.receive(_) >> "ok" // subscriber is a Stub()

Mock: используется для описания взаимодействия между объектом спецификации и его участниками.

def "should send message to subscriber"() {
    when:
        publisher.send("hello")

    then:
        1 * subscriber.receive("hello") // subscriber is a Mock()
}

Mock может действовать как Mock и Stub:

1 * subscriber.receive("message1") >> "ok" // subscriber is a Mock()

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

def subscriber = Spy(SubscriberImpl, constructorArgs: ["Fred"])

def "should send message to subscriber"() {
    when:
        publisher.send("hello")

    then:
        1 * subscriber.receive("message1") >> "ok" // subscriber is a Spy(), used as a Mock an Stub
}

def "should send message to subscriber (actually handle 'receive')"() {
    when:
        publisher.send("hello")

    then:
        1 * subscriber.receive("message1") // subscriber is a Spy(), used as a Mock, uses real 'receive' function
}

Резюме:

  • Заглушка () - это заглушка.
  • Mock () - это заглушка и макет.
  • Шпион () - это заглушка, насмешка и шпион.

Избегайте использования Mock (), если достаточно Stub ().

По возможности избегайте использования Spy (), это может быть запахом и намеком на неправильный тест или неправильный дизайн тестируемого объекта.


1
Просто добавлю: еще одна причина, по которой вы хотите минимизировать использование макетов, заключается в том, что макет очень похож на утверждение, поскольку вы проверяете вещи на макете, который может не пройти тест, и вы всегда хотите минимизировать количество проверок. вы делаете это в тесте, чтобы тест был сфокусированным и простым. Так что в идеале на тест должен быть только один макет.
Sammi

1
«Шпион () - это заглушка, насмешка и шпион». это не правда для шпионов Sinon?
basickarl

2
Я только что взглянул на шпионов Sinon, и они выглядят так, как будто они не ведут себя как моки или заглушки. Обратите внимание, что этот вопрос / ответ относится к Spock, который является Groovy, а не JS.
Михаил

Это должен быть правильный ответ, поскольку он привязан к контексту Спока. Кроме того, утверждение, что заглушка является причудливым макетом, может вводить в заблуждение, поскольку макет имеет дополнительную функциональность (проверка количества вызовов), которой нет у заглушки (макет> причудливее, чем заглушка). Опять же, макет и заглушки как у Спока.
CGK

13

Проще говоря:

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

Заглушка: вы создаете класс-заглушку, в котором методы переопределяются с определением в соответствии с вашими требованиями. Пример: в реальном методе объекта вы вызываете и внешний API и возвращаете имя пользователя и идентификатор. В методе заглушенного объекта вы возвращаете фиктивное имя.

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

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


0

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

Итак, допустим, у вас есть метод, который принимает объект в качестве параметра. Вы никогда не делаете ничего, что изменяет этот параметр в тесте. Вы просто считываете из него значение. Это заглушка.

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.