Різниця між Mock / Stub / Spy в рамках тесту Спока


101

Я не розумію різниці між Mock, Stub та Spy у Spock тестуванні та навчальні посібники, які я переглядав в Інтернеті, не пояснюють їх докладно.

Відповіді:


94

Увага: я збираюся спростити і, можливо, навіть трохи підробити в наступних пунктах. Більш детальну інформацію див. На веб-сайті Мартіна Фаулера .

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

Заглушка - це також фіктивний клас, який дає деякі більш конкретні, підготовлені або попередньо записані, відтворені результати для певних тестових запитів. Можна сказати, заглушка - це химерний глуз. У Spock ви часто читатимете про методи заглушки.

Шпигун - це різновид гібриду між реальним об'єктом і заглушкою, тобто в основному це реальний об'єкт з деякими (не всіма) методами, затіненими методами заглушки. Несмучені методи просто перенаправляються до оригінального об'єкта. Таким чином, ви можете мати оригінальну поведінку для «дешевих» або тривіальних методів та фальшиву поведінку для «дорогих» або складних методів.


Оновлення 2017-02-06: Насправді відповідь користувача mikhail є більш специфічною для Спока, ніж моя первісна вище. Тож у межах Спока те, що він описує, є правильним, але це не підробляє мою загальну відповідь:

  • Заглушка займається імітацією конкретної поведінки. У Spock це все, що може зробити заглушка, тож це найпростіша річ.
  • Макет стосується того, щоб стояти за (можливо, дорогим) реальним об'єктом, надаючи необоротні відповіді на всі виклики методу. У цьому плані макет простіший за заглушку. Але в Spock, макет може також заглушити результати методу, тобто бути і макетом, і заглушкою. Крім того, у 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?")
  }
}

Різниця між макетом та заглушкою тут не зрозуміла. За допомогою насмішок хочеться перевірити поведінку (якщо і скільки разів метод буде називатися). За допомогою заглушок можна перевірити лише стан (наприклад, розмір колекції після тесту). FYI: макети також можуть забезпечити підготовлені результати.
chipiik

Дякуємо @mikhail та chipiik за відгук. Я оновив свою відповідь, сподіваюся, покращуючи та уточнюючи кілька речей, які я спочатку написав. Відмова: У моїй оригінальній відповіді я сказав, що я надто спрощував і трохи фальсифікував деякі факти, пов'язані зі Споком. Мені хотілося, щоб люди зрозуміли основні відмінності між заклинанням, глузуванням та шпигунством.
kriegaex

@chipiik, ще одна річ як відповідь на ваш коментар: Я вже багато років треную команди з розвитку та бачу, як вони використовують Спок або інший JUnit з іншими макетними рамками. У більшості випадків, коли використовують макети, вони робили це не для того, щоб перевірити поведінку (тобто виклики методу підрахунку), а щоб ізолювати випробуваного від його оточення. Підрахунок взаємодії IMO - це лише додаткова вигода, і її слід використовувати продумано та щадно, тому що існує тенденція до того, що такі тести руйнуються, коли вони перевіряють проводку компонентів більше, ніж їх реальна поведінка.
kriegaex

Його коротка, але все ж дуже корисна відповідь
Chaklader Asfak Arefe

55

Питання було в контексті Спока, і я не вірю, що нинішні відповіді враховують це.

Спираючись на документи Spock (приклади налаштовані, додано моє власне формулювання):

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

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

Макет: використовується для опису взаємодій між об'єктом, що знаходиться під специфікацією, та його співавторами.

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

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

Знущання може виступати як глузування та заглушка:

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

Шпигун: завжди базується на реальному об'єкті з оригінальними методами, які роблять реальні речі. Можна використовувати як Stub для зміни повернутих значень вибраних методів. Може використовуватися як Макет для опису взаємодій.

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
}

Підсумок:

  • А Stub () - це Stub.
  • Макет () - це Стуб і Макет.
  • Шпигун () - це Стуб, Макет і Шпигун.

Уникайте використання Mock (), якщо Stub () є достатнім.

Уникайте використання Spy (), якщо можете, якщо це зробити, це може бути запахом і натяком на неправильний тест або неправильну конструкцію тестуваного об'єкта.


1
Додамо лише: Ще одна причина, з якої ви хочете мінімізувати використання макетів, полягає в тому, що макет дуже схожий на твердження, що ви перевіряєте речі на макет, які можуть не виконати тест, і ви завжди хочете мінімізувати кількість перевірок. Ви робите тест, щоб тест був зосередженим та простим. Тож в ідеалі має бути лише один макет за тест.
Саммі

1
"Шпигун () - це Стуб, Макет і Шпигун." це не вірно для шпигунів-синонів?
K - Токсичність в SO зростає.

2
Я щойно поглянув на шпигунів Сінона, і вони виглядають так, ніби вони не поводяться як Макети чи Стуби. Зауважте, що це питання / відповідь знаходяться в контексті Спока, який є Groovy, а не JS.
mikhail

Це має бути правильною відповіддю, оскільки це стосується контексту Спока. Крім того, сказати, що заглушка - це химерний макет, може ввести в оману, оскільки макет має додаткову функціональність (перевірка кількості викликів), що заглушка не має (глузувати> вигадливо, ніж заглушка). Знову ж таки, знущання та заглушки відповідно до Спока.
CGK

13

Простими словами:

Знущання: Ви знущаєтесь над типом і на ходу отримуєте створений об’єкт. Методи в цьому макетному об’єкті повертають типові значення типу повернення.

Стуб: Ви створюєте клас заглушки, де методи переосмислюються із визначенням відповідно до вашої вимоги. Наприклад: у реальному об'єктному методі ви викликаєте і зовнішній api та повертаєте ім'я користувача проти та id. У методі стертого об’єкта ви повертаєте якесь манекенне ім'я.

Шпигун: Ви створюєте один реальний об'єкт, а потім шпигуєте за ним. Тепер ви можете знущатися над деякими методами і вирішувати не робити цього для деяких.

Одна відмінність використання полягає в тому, що ви не можете знущатися над об'єктами рівня методу. тоді як ви можете створити об'єкт за замовчуванням у методі, а потім шпигувати за ним, щоб отримати бажану поведінку методів у шпигуваному об'єкті.


0

Заглушки дійсно лише для полегшення одиничного тесту, вони не є частиною тесту. Макети, частина тесту, частина перевірки, частина проходження / відмови.

Так, скажімо, у вас є метод, який приймає об’єкт як параметр. Ви ніколи не робите нічого, що змінює цей параметр у тесті. Ви просто читаєте значення з нього. Це заглушка.

Якщо ви щось змінили або вам потрібно перевірити якусь взаємодію з об’єктом, то це - насмішка.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.