Моки, стабы, пустышки, шпионы и фейки. Полный гайд по тестовым дублерам

Что такое тестовые дублеры

При написании тестов часто возникают ситуации, когда необходимо смоделировать или заменить определенные зависимости, чтобы изолировать поведение тестируемого кода. Для этого и предназначены тестовые дублеры (Test Doubles).

Это мощные средства, используемые для подмены тестируемых объектов. Эти «заменители» позволяют изучать поведение зависимостей во время тестирования.

Пять типов тестовых дублеров:

  • Пустышка (Dummy): Используется в качестве «заполнителя» вместо аргумента.
  • Стаб (Stub): Предоставляет сгенерированные данные тестируемой системе.
  • Шпион (Spy): записывает информацию о том, как используется класс.
  • Мок (Mock): Описывает ожидание, если оно не совпало с нужным, выдает сбой.
  • Фейк (Fake): Имплементация контракта, но не подходит для production.

Когда нужны тестовые дублеры

Существуют ситуации, когда они незаменимы. Один из распространенных случаев — когда приложению нужны внешние сервисы, базы данных или API. Доступ к этим сервисам во время тестирования зависит от их постоянной доступности, и производительности. Используя дублеры, можно избежать этой зависимости и обеспечить изолированность.

Еще одна ситуация, в которой могут понадобиться дублеры — когда определенные пути в коде труднодоступны, или когда нужно моделировать условия, которые трудно воспроизвести. Например, моделирование отключения интернета, или операций критичных по времени, или каких-то исключительных условий. Дублеры позволяют создавать контролируемые тестовые окружения, имитирующие подобные сценарии; это дает возможность тщательно проверить надежность кода и обработку граничных случаев (edge cases).

Применение 

Основная мотивация использования тестовых дублеров — отделить тестируемый код от его зависимостей, что позволяет тестировать модули и компоненты изолированно. Заменив реальные объекты на их дублеры, мы получаем контроль над их поведением, гарантируя, что тесты сфокусированы исключительно на тестируемом компоненте. Изоляция помогает эффективнее выявлять дефекты, в том числе регрессионные, и упрощает отладку, если источник ошибок локализован в конкретном модуле.

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

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

(Примеры в тексте — на языке Go.)

Пустышки (Dummies)

Что такое пустышки?

Самая простая форма тестовых дублеров. По сути, это или пустые, или минимальные имплементации объектов, которые требуются в качестве аргументов методов или коллабораторов (объектов, используемых целевым тестируемым объектом), но не влияют на поведение тестируемого модуля. Пустышки используются исключительно для того, чтобы «удовлетворить компилятор» или выполнить ожидания параметров, позволяя тестовому коду успешно выполниться.

Когда используются Dummies

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

Пример

Представьте, что у вас есть интерфейс Logger, отвечающий за запись сообщений в лог, и вы хотите протестировать структуру (struct) Calculator, которая выполняет какие-то вычисления и записывает результаты в лог. В этом случае можете использовать фиктивную реализацию интерфейса Logger через пустышки, чтобы «удовлетворить» эту зависимость без фактической записи в лог:

type Logger interface {
    Log(message string)
}

type Calculator struct {
    logger Logger
}

func (c *Calculator) Add(a, b int) int {
    sum := a + b
    c.logger.Log(fmt.Sprintf("Addition: %d + %d = %d", a, b, sum))
    return sum
}

В своем тестовом сценарии вы можете создать пустышку интерфейса Logger, которая не выполняет никаких записей:

type DummyLogger struct{}

func (d *DummyLogger) Log(message string) {
    // Do nothing
}

Теперь при тестировании метода Add в Calculator, вы можете использовать DummyLogger как замену зависимости Logger:

import (
    "testing"
)

func TestCalculator_Add(t *testing.T) {
    dummyLogger := &DummyLogger{}
    calculator := &Calculator{logger: dummyLogger}

    result := calculator.Add(2, 3)
    expected := 5

    if result != expected {
        t.Errorf("Addition result incorrect. Got %d, expected %d", result, expected)
    }
}

В этом примере DummyLogger выступает в качестве заполнителя места (placeholder), который удовлетворяет зависимость Logger, не выполняя фактического действия (записи). Это позволяет сосредоточиться на тестировании логики Calculator, не отвлекаясь на второстепенную задачу.

Как видим, использование пустышки в таких сценариях позволяет изолировать тестируемый модуль и упрощает процесс, без имплементации Logger.

Стабы (Stubs)

Что такое стабы

Тестовые дублеры, которые позволяют подменять зависимости и контролировать их поведение во время тестирования. В отличие от предыдущих пустышек, которые представляют собой «пустые» или самые минимальные реализации, стабы обеспечивают предопределенные ответы или поведение при вызове методов. Используя стабы, мы можем моделировать различные условия и сценарии, например, возвращать определенные значения, вызывать исключения и имитировать задержки.

Когда использовать стабы

Нужны в ситуациях, когда тестируемый код зависит от внешних сервисов, баз данных или API, которые могут быть недоступны или нежелательны во время тестирования. Заменив эти зависимости на стабы, можно имитировать их поведение и ответы, делая тесты более изолированными и предсказуемыми. Стабы также пригодятся, когда нужно протестировать обработку ошибок или исключительные ситуации, которые сложно воспроизвести в реальных условиях.

Пример использования

Рассмотрим упрощенный пример, в котором у нас есть интерфейс WeatherService, отвечающий за получение данных о погоде, и структура WeatherReporter, которая использует этот сервис для передачи текущего статуса погоды.

type WeatherService interface {
    GetWeather(city string) (string, error)
}

type WeatherReporter struct {
    weatherService WeatherService
}

func (wr *WeatherReporter) ReportWeather(city string) string {
    weather, err := wr.weatherService.GetWeather(city)
    if err != nil {
        return "Failed to retrieve weather data."
    }
    return "Current weather: " + weather
}

В нашем тестовом сценарии мы хотим убедиться, что метод ReportWeather правильно обрабатывает кейс, когда метод GetWeather возвращает ошибку. Для имитации этого сценария используем стаб-реализацию сервиса WeatherService:

import (
    "testing"
    "errors"
)

type StubWeatherService struct{}

func (sws *StubWeatherService) GetWeather(city string) (string, error) {
    return "", errors.New("API error: failed to retrieve weather data")
}

func TestWeatherReporter_ReportWeather_Error(t *testing.T) {
    stubService := &StubWeatherService{}
    weatherReporter := &WeatherReporter{weatherService: stubService}

    result := weatherReporter.ReportWeather("New York")

    expected := "Failed to retrieve weather data."
    if result != expected {
        t.Errorf("ReportWeather returned %q, expected %q", result, expected)
    }
}

В приведенном выше примере мы создаем стаб интерфейса WeatherService под названием StubWeatherService. Метод GetWeather имплементации стаба всегда возвращает ошибку. Используя этот стаб в нашем тесте, мы имитируем сценарий, когда служба погоды не может получить данные о погоде. Затем проверяем, что метод ReportWeather правильно обрабатывает это состояние ошибки. 

Итак, стабы — мощные тестовые дублеры, которые позволяют заменять зависимости и контролировать их поведение. Как говорилось выше, они предоставляют прописанные реакции на события (поведение), позволяя моделировать тестовые сценарии и более гибко проверять условия. Используя стабы, мы эффективно изолируем тестируемый код и проверяем, что он ведет себя правильно в различных ситуациях, включая обработку ошибок и исключительные случаи.

Шпионы (Spies)

Что такое шпионы?

Тестовые дублеры, которые выступают в качестве «прокси» для зависимостей, позволяя наблюдать за ними и проверять, как они используются во время тестирования. В отличие от предыдущих пустышек и стабов, которые фокусируются на требованиях к параметрам или предопределенных ответах, шпионы являются инструментом сбора информации о вызовах методов — количество вызовов, переданные аргументы или порядок вызова методов. Используя этот подвид дублеров, мы получаем представление о взаимодействии между тестируемым кодом и его коллабораторами.

Когда используются

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

Пример использования

Рассмотрим упрощенный пример, в котором у нас есть интерфейс PaymentGateway, отвечающий за обработку платежных транзакций, и структура PaymentProcessor, которая использует этот шлюз для инициирования платежей.

type PaymentGateway interface {
    ProcessPayment(amount float64, currency string) error
}

type PaymentProcessor struct {
    paymentGateway PaymentGateway
}

func (pp *PaymentProcessor) MakePayment(amount float64, currency string) error {
    return pp.paymentGateway.ProcessPayment(amount, currency)
}

В этом тестовом сценарии мы хотим убедиться, что метод MakePayment корректно вызывает метод ProcessPayment на PaymentGateway. Можем использовать имплементацию шпиона PaymentGateway, чтобы перехватить и проверить это взаимодействие:

import (
    "testing"
)

type SpyPaymentGateway struct {
    processPaymentCalled bool
    lastAmount           float64
    lastCurrency         string
}

func (spy *SpyPaymentGateway) ProcessPayment(amount float64, currency string) error {
    spy.processPaymentCalled = true
    spy.lastAmount = amount
    spy.lastCurrency = currency
    return nil
}

func TestPaymentProcessor_MakePayment(t *testing.T) {
    spyGateway := &SpyPaymentGateway{}
    paymentProcessor := &PaymentProcessor{paymentGateway: spyGateway}

    amount := 100.0
    currency := "USD"
    err := paymentProcessor.MakePayment(amount, currency)

    if !spyGateway.processPaymentCalled {
        t.Error("ProcessPayment not called")
    }

    if spyGateway.lastAmount != amount {
        t.Errorf("ProcessPayment called with amount %f, expected %f", spyGateway.lastAmount, amount)
    }

    if spyGateway.lastCurrency != currency {
        t.Errorf("ProcessPayment called with currency %s, expected %s", spyGateway.lastCurrency, currency)
    }

    if err != nil {
        t.Errorf("MakePayment returned error: %v", err)
    }
}

В приведенном выше примере мы создаем имплементацию шпиона интерфейса PaymentGateway под названием SpyPaymentGateway. Шпион записывает информацию о вызовах своих методов, включая факт вызова ProcessPayment, а также переданные ему сумму и валюту. В тесте мы проверяем, что метод MakePayment правильно взаимодействует с PaymentGateway, изучая полученную информацию.

Моки (Mocks)

Что это

Тестовые дублеры, которые имитируют поведение реальных зависимостей, предоставляя возможность определять ожидания и проверять взаимодействия. В отличие от стабов и шпионов, которые сосредоточены на предопределенных ответах или перехвате вызовов методов, моки позволяют определить ожидаемую последовательность вызовов методов, параметров и возвращаемых значений. Они позволяют создавать хорошо контролируемые тестовые сценарии, прописывая, как зависимости должны вести себя во время тестирования.

Когда используются

Когда хотят тщательно протестировать взаимодействие между тестируемым кодом и его зависимостями. Используя моки, мы можем точно определить ожидания относительно вызовов методов, их параметров и возвращаемых значений. Такой уровень контроля позволяет тестировать сложную логику, пограничные случаи (edge cases) и гарантировать, что код правильно обрабатывает различные сценарии. Моки помогают лучше изолировать тестируемый модуль/компонент, поскольку мы можем заменить его зависимости на моки, предотвращая нежелательные побочные эффекты во время тестирования.

Пример использования

Рассмотрим упрощенный пример, в котором у нас есть интерфейс EmailSender, отвечающий за отправку уведомлений по электронной почте, и структура UserManager, которая использует этот отправитель для уведомления пользователей.

type EmailSender interface {
    SendEmail(address, subject, body string) error
}

type UserManager struct {
    emailSender EmailSender
}

func (um *UserManager) SendWelcomeEmail(email string) error {
    subject := "Welcome to our platform!"
    body := "Thank you for joining. We're excited to have you on board."
    return um.emailSender.SendEmail(email, subject, body)
}

В этом тестовом сценарии мы хотим убедиться, что метод SendWelcomeEmail правильно вызывает метод SendEmail интерфейса EmailSender. Для определения ожиданий и проверки взаимодействия используем мок-имплементацию EmailSender:

import (
    "testing"

    "github.com/stretchr/testify/mock"
)

type MockEmailSender struct {
    mock.Mock
}

func (mock *MockEmailSender) SendEmail(address, subject, body string) error {
    args := mock.Called(address, subject, body)
    return args.Error(0)
}

func TestUserManager_SendWelcomeEmail(t *testing.T) {
    mockSender := &MockEmailSender{}
    userManager := &UserManager{emailSender: mockSender}

    email := "test@example.com"
    expectedSubject := "Welcome to our platform!"
    expectedBody := "Thank you for joining. We're excited to have you on board."

    mockSender.On("SendEmail", email, expectedSubject, expectedBody).Return(nil)

    err := userManager.SendWelcomeEmail(email)

    if err != nil {
        t.Errorf("SendWelcomeEmail returned error: %v", err)
    }

    mockSender.AssertExpectations(t)
}

В приведенном выше примере мы создаем мок-реализацию интерфейса EmailSender под названием MockEmailSender. Используя пакет github.com/stretchr/testify/mock, мы определяем ожидания для метода SendEmail с определенными параметрами. Затем мы используем метод AssertExpectations, чтобы убедиться, что все ожидания были выполнены во время тестирования.

Фейки (Fakes)

Что это

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

Когда использовать

Когда реальная имплементация зависимости непрактична или нежелательна во время тестирования; это может быть вызвано такими причинами, как сетевые зависимости, внешние сервисы или сложная бизнес-логика. Используя фейки, можно имитировать поведение зависимости контролируемым образом, что делает тесты предсказуемыми, изолированными и эффективными. Также, когда нужно протестировать сценарии, которые сложно воспроизвести последовательно.

Пример использования

У нас есть интерфейс FileStore, отвечающий за хранение и получение файлов, и структура FileManager, которая использует это хранилище для выполнения операций с файлами.

type FileStore interface {
    StoreFile(filename string, data []byte) error
    RetrieveFile(filename string) ([]byte, error)
}

type FileManager struct {
    fileStore FileStore
}

func (fm *FileManager) SaveFile(filename string, data []byte) error {
    return fm.fileStore.StoreFile(filename, data)
}

func (fm *FileManager) ReadFile(filename string) ([]byte, error) {
    return fm.fileStore.RetrieveFile(filename)
}

В тестовом сценарии мы хотим убедиться, что FileManager корректно взаимодействует с FileStore при сохранении и чтении файлов. Используем имплементацию FileStore в виде фейка, чтобы обеспечить упрощенное поведение для тестирования:

import (
    "testing"
)

type FakeFileStore struct {
    storedFiles map[string][]byte
}

func (fake *FakeFileStore) StoreFile(filename string, data []byte) error {
    fake.storedFiles[filename] = data
    return nil
}

func (fake *FakeFileStore) RetrieveFile(filename string) ([]byte, error) {
    data, exists := fake.storedFiles[filename]
    if !exists {
        return nil, fmt.Errorf("File not found: %s", filename)
    }
    return data, nil
}

func TestFileManager_SaveFile_ReadFile(t *testing.T) {
    fakeStore := &FakeFileStore{storedFiles: make(map[string][]byte)}
    fileManager := &FileManager{fileStore: fakeStore}

    // Save file
    filename := "test.txt"
    data := []byte("Hello, World!")
    err := fileManager.SaveFile(filename, data)
    if err != nil {
        t.Errorf("SaveFile returned error: %v", err)
    }

    // Read file
    retrievedData, err := fileManager.ReadFile(filename)
    if err != nil {
        t.Errorf("ReadFile returned error: %v", err)
    }
    if !bytes.Equal(retrievedData, data) {
        t.Errorf("Retrieved data does not match expected data")
    }
}

В приведенном примере мы создаем имплементацию интерфейса FileStore под названием FakeFileStore. Реализация фейка упрощает поведение, сохраняя файлы в памяти с помощью map. Во время тестирования мы можем сохранить файл с помощью метода SaveFile и считать его с помощью метода ReadFile. Затем мы можем проверить через assertion, что считанные данные совпадают с ожидаемыми.

Советы по выбору и применению тестовых дублеров (таблица)

Тестовый дублерПредназначениеПоведениеПроверка взаимодействияЗапись внутреннего поведенияЮз-кейсы в реальных проектахВерификация
Пустышка Объект-заполнительНичего не делаетНетНетКогда параметр требуется, но не используется в тестеНе применимо
СтабДавать заранее определенные ответыВозвращает фиксированные значенияНетНетМоделирование простого поведения или уменьшение зависимостейНе применимо
МокУстановить ожидания от взаимодействияВозвращает заранее заданные значенияДаНетПроверка того, как объект взаимодействует со своими зависимостямиПроверяет, произошло ли ожидаемое взаимодействие
ФейкАльтернативная упрощенная реализацияВоспроизводит некоторые реальные действияНетНетЗамена ресурсоемких зависимостей для ускорения тестированияМожет не требовать явной проверки
ШпионЗапись взаимодействий и параметровВозвращает фактические данные, но записывает вызовыДаНетНаблюдение и регистрация внутренних взаимодействийМожет использоваться для утверждения ожидаемого поведения

Ключевые характеристики тестовых дублеров

Пустышка

  • Предоставляет валидный объект для выполнения требований в методе
  • Не влияет на результат тестирования, поскольку не участвует в логике тестирования
  • Часто используется в ситуациях, когда аргумент необходим, но не влияет на поведение теста

Стаб

  • Возвращает фиксированные значения или исключения при вызове метода
  • Используется, когда необходимо изолировать код от сложных внешних зависимостей
  • Подходит для эмуляции read-only-операций или методов с предсказуемым поведением

Мок

  • Устанавливает ожидания для вызовов методов и параметров
  • Проверяет, были ли вызваны определенные методы и сколько раз
  • Может выбрасывать исключения на основе заранее определенных условий
  • Помогает в тестировании паттернов взаимодействия и обеспечении надлежащего взаимодействия между объектами

Фейк

  • Предоставляет альтернативную реализацию зависимости с упрощенной функциональностью
  • Может использоваться для замены медленного или ресурсоемкого компонента на более легкую и быструю версию
  • Часто используется для тестирования баз данных, файловых систем или внешних сервисов, когда настройка реального компонента непрактична или требует много времени

Шпион

  • Действует как обертка вокруг реального объекта для отслеживания вызовов методов и их параметров
  • Записывает взаимодействия и шаблоны использования во время тестирования
  • Полезно, когда нужно проверить как результат, так и то, как он был достигнут
  • Дает представление о том, как тестируемый объект используется в приложении

Тестовый дублер — обобщенно

  • Общий термин для любого объекта, который заменяет реальную зависимость при тестировании
  • Может обозначать любой из объектов типа dummy, stub, mock, fake, или spy
  • Все они обеспечивают изоляцию тестов и фокусируются на определенных компонентах или поведении

Источник

Видео по теме (англ)


Моки и стабы — краткий гайд

Какой была ваша первая зарплата в QA и как вы искали первую работу?

Мега обсуждение в нашем телеграм-канале о поиске первой работы. Обмен опытом и мнения.

Подписаться
Уведомить о
guest

0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии

Мы в Telegram

Наш официальный канал
Полезные материалы и тесты
Готовимся к собеседованию
Project- и Product-менеджмент

? Популярное

? Telegram-обсуждения

Наши подписчики обсуждают, как искали первую работу в QA. Некоторые ищут ее прямо сейчас.
Наши подписчики рассказывают о том, как не бояться задавать тупые вопросы и чувствовать себя уверенно в новой команде.
Обсуждаем, куда лучше податься - в менеджмент или по технической ветке?
Говорим о конфликтных ситуациях в команде и о том, как их избежать
$1100*
медианная зарплата в QA в июне 2023

*по результатам опроса QA-инженеров в нашем телеграм-канале

Собеседование

19%*
IT-специалистов переехало или приняло решение о переезде из России по состоянию на конец марта 2022

*по результатам опроса в нашем телеграм-канале

live

Обсуждают сейчас