Мокирование данных mocking в тестировании

05 января 2026
Автор

Олег Марков

Введение

Мокирование данных (mocking) — это техника, которая позволяет вам заменить реальные зависимости в коде на контролируемые тестовые объекты. С помощью моков вы подменяете, например, реальные обращения к базе данных, HTTP-запросы или интеграцию с внешними сервисами на предсказуемое поведение, которое полностью под вашим контролем.

Смотрите, я покажу вам, как эта идея помогает:

  • изолировать тестируемый код;
  • ускорить тесты;
  • воспроизводить сложные или редкие сценарии (ошибки сети, таймауты, нестабильные сервисы);
  • не зависеть от окружения (настроенной БД, доступного API и т.п.).

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

Что такое мокирование и зачем оно нужно

Основная идея мокирования

Представьте, что у вас есть функция, которая:

  • читает данные из базы данных;
  • отправляет эти данные во внешний HTTP‑сервис;
  • возвращает результат в виде ответа пользователю.

В юнит‑тесте вы, как правило, не хотите:

  • поднимать реальную базу данных;
  • ждать реальный HTTP‑запрос;
  • зависеть от сети и работы стороннего сервиса.

Вместо этого вы подменяете настоящий объект доступа к БД и настоящий HTTP‑клиент на моки — тестовые объекты, которые:

  • возвращают заранее определённые данные;
  • фиксируют, какие методы были вызваны, с какими аргументами;
  • могут эмулировать ошибки, таймауты и другие нестандартные ситуации.

Мокирование — это как создание «театральных декораций» для вашего кода. Внешний мир выглядит «как настоящий», но всё под контролем и не выходит за границы теста.

Задачи, которые решает мокирование

Основные задачи, которые вы закрываете с помощью моков:

  • Изоляция кода в юнит‑тестах.
  • Управляемость поведения зависимостей (успех/ошибка, задержки, нестабильность).
  • Детерминированность тестов (одни и те же входные данные — один и тот же результат).
  • Повышение скорости тестов (нет реальных сетевых/дисковых операций).
  • Проверка взаимодействия между компонентами (вызывается ли нужный метод, с нужными параметрами, нужное количество раз).

Виды тестовых двойников: моки, стабы, фейки, шпионы

Часто под словом «мок» подразумевают любой тестовый двойник, но давайте аккуратно разделим основные типы — так вам будет легче выбирать правильный инструмент под задачу.

Stub (стаб)

Стаб — это простой объект, который подставляет готовые ответы. Он не «знает», вызывали его или нет, его задача — только вернуть нужные данные.

Применение:

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

Пример использования стаба (на псевдокоде):

// Интерфейс источника данных
type UserRepository interface {
    FindByID(id int) (User, error)
}

// Стаб репозитория - всегда возвращает одного и того же пользователя
type StubUserRepository struct{}

func (s StubUserRepository) FindByID(id int) (User, error) {
    // Здесь мы не обращаемся к базе данных
    return User{ID: id, Name: "Test User"}, nil
}

Здесь стаб просто возвращает фиксированный результат. Мы не проверяем, сколько раз и с какими ID его вызывали.

Fake (фейк)

Фейк — это реализация интерфейса, которая ведет себя «почти как настоящая», но проще и «в памяти». Например:

  • in‑memory база данных;
  • локальный кэш вместо распределенного;
  • упрощенный файловый сторедж.

Фейк часто имеет какую‑то простую, но рабочую внутреннюю логику.

// Фейковая in-memory реализация
type FakeUserRepository struct {
    users map[int]User
}

func NewFakeUserRepository() *FakeUserRepository {
    return &FakeUserRepository{
        users: map[int]User{},
    }
}

func (f *FakeUserRepository) Save(u User) error {
    // Здесь мы не обращаемся к реальной БД - просто сохраняем в map
    f.users[u.ID] = u
    return nil
}

func (f *FakeUserRepository) FindByID(id int) (User, error) {
    // Эмулируем простое чтение из "БД" в памяти
    if u, ok := f.users[id]; ok {
        return u, nil
    }
    return User{}, errors.New("not found")
}

Фейк хорош, если вы хотите немного логики (сохранение, поиск), но не хотите реальной инфраструктуры.

Mock (мок)

Мок — это тестовый объект, который:

  • умеет возвращать заданные значения;
  • фиксирует информацию о вызовах;
  • часто проверяет ожидания (какие вызовы должны были произойти).

Мок позволяет сказать: «Ожидаю, что метод SendEmail будет вызван один раз с аргументом user@example.com».

// Интерфейс почтового сервиса
type Mailer interface {
    SendEmail(to string, subject string, body string) error
}

// Ручной мок для проверки вызова
type MockMailer struct {
    SentTo      []string
    ShouldError bool
}

func (m *MockMailer) SendEmail(to string, subject string, body string) error {
    // Сохраняем, кому отправляли - это поможет нам потом проверить вызовы
    m.SentTo = append(m.SentTo, to)
    if m.ShouldError {
        // Эмулируем ошибку, если включен флаг
        return errors.New("send failed")
    }
    return nil
}

Здесь вы можете в тесте:

  • задать ShouldError = true, чтобы симулировать сбой отправки;
  • проверить, какой список адресов оказался в SentTo.

Spy (шпион)

Шпион — это объект, который главным образом служит для записи информации о вызовах:

  • какие методы вызывались;
  • с какими параметрами;
  • сколько раз.

Фактически в ручной реализации мок и шпион очень похожи. Разница в акцентах: шпион — про «наблюдение», мок — про «ожидания и поведение».

Когда стоит и когда не стоит использовать моки

Когда моки особенно полезны

Используйте моки, если:

  • Вы пишете юнит‑тесты для бизнес‑логики, которая сильно зависит от внешних сервисов.
  • Ваша логика должна правильно реагировать на разные ошибки внешних зависимостей (HTTP 500, таймауты, недоступность БД).
  • Реальная зависимость медленная или нестабильная.
  • Важно проверить, что ваш код «правильно взаимодействует» с зависимостью, а не только результат.

Например, вы хотите проверить, что:

  • при неудачной оплате не отправляется e‑mail;
  • при успешной оплате отправляется два разных письма.

Это удобно делать через моки и проверки их состояния после выполнения кода.

Когда лучше обойтись без моков

Есть ситуации, когда моки только усложняют код:

  • Простая функция, которая почти не имеет внешних зависимостей (логика чистая, без IO).
  • Интеграционные тесты, где вы хотите проверить работу с реальной БД или реальным API (на тестовом стенде).
  • Тесты, которые должны гарантировать совместимость со сторонним сервисом по реальному протоколу.

Иногда проще поднять тестовую БД (например, через Docker) и писать интеграционные тесты без моков — это уменьшит расхождение с реальностью. Хорошая стратегия — комбинировать:

  • юнит‑тесты с моками;
  • интеграционные тесты с реальными зависимостями.

Основные подходы к мокированию

Ручное мокирование через интерфейсы

Самый понятный и контролируемый подход — вы определяете интерфейсы для зависимостей и создаете их ручные реализации для тестов.

Давайте разберемся на примере на Go. В других языках идею можно повторить 1‑в‑1.

Боевая реализация и интерфейс

// Интерфейс клиента внешнего сервиса
type PaymentGateway interface {
    Charge(amount int, cardToken string) (string, error)
}

// Реальная реализация - ходит во внешний API
type HttpPaymentGateway struct {
    // Здесь может быть http.Client и настройки
}

func (g *HttpPaymentGateway) Charge(amount int, cardToken string) (string, error) {
    // Здесь мы бы сформировали HTTP-запрос
    // отправили его и обработали ответ
    // В тестах это нам не нужно - поэтому будем подменять этот объект
    return "", nil
}

Код, который зависит от интерфейса

// Сервис платежей использует интерфейс PaymentGateway
type PaymentService struct {
    gateway PaymentGateway
}

func NewPaymentService(g PaymentGateway) *PaymentService {
    // Здесь мы внедряем зависимость через конструктор - это удобно мокировать
    return &PaymentService{gateway: g}
}

func (s *PaymentService) Pay(amount int, token string) (string, error) {
    // Здесь мы просто делегируем работу гейтвею и обрабатываем результат
    txID, err := s.gateway.Charge(amount, token)
    if err != nil {
        return "", fmt.Errorf("payment failed: %w", err)
    }
    return txID, nil
}

Ручной мок для PaymentGateway

// MockPaymentGateway - мок для эмуляции поведения внешнего платежного сервиса
type MockPaymentGateway struct {
    // Поля для контроля поведения и фиксации вызовов
    ShouldError bool      // Эмулировать ли ошибку
    LastAmount  int       // Последняя сумма, которую "списали"
    LastToken   string    // Последний токен карты
    TxIDToReturn string   // Какой ID транзакции вернуть
    CallCount   int       // Сколько раз вызывался метод Charge
}

func (m *MockPaymentGateway) Charge(amount int, cardToken string) (string, error) {
    // Сохраняем параметры вызова
    m.LastAmount = amount
    m.LastToken = cardToken
    m.CallCount++

    if m.ShouldError {
        // Эмулируем ошибочный ответ внешнего сервиса
        return "", errors.New("gateway unavailable")
    }

    // Возвращаем заранее заданный идентификатор транзакции
    if m.TxIDToReturn == "" {
        // Если ID не задан, поставим значение по умолчанию
        m.TxIDToReturn = "tx_default"
    }
    return m.TxIDToReturn, nil
}

Тест с использованием ручного мока

func TestPaymentService_Pay_Success(t *testing.T) {
    // Здесь мы создаем мок платежного гейтвея
    mockGateway := &MockPaymentGateway{
        ShouldError:  false,         // Ошибку не эмулируем
        TxIDToReturn: "tx_12345",    // Задаем ожидаемый ID транзакции
    }

    // Внедряем мок в сервис вместо реальной реализации
    service := NewPaymentService(mockGateway)

    // Вызываем боевой код
    txID, err := service.Pay(100, "token_abc")

    if err != nil {
        // Если сюда попали, значит бизнес-логика отработала неверно
        t.Fatalf("unexpected error: %v", err)
    }

    if txID != "tx_12345" {
        // Проверяем, что мы получили ожидаемый ID транзакции
        t.Errorf("expected txID tx_12345, got %s", txID)
    }

    if mockGateway.CallCount != 1 {
        // Убеждаемся, что шлюз вызывался ровно один раз
        t.Errorf("expected gateway to be called once, got %d", mockGateway.CallCount)
    }

    if mockGateway.LastAmount != 100 || mockGateway.LastToken != "token_abc" {
        // Проверяем, что метод вызывался с правильными аргументами
        t.Errorf("unexpected args amount=%d token=%s", mockGateway.LastAmount, mockGateway.LastToken)
    }
}

Обратите внимание, что:

  • мы полностью контролируем поведение внешнего сервиса;
  • можем проверять, как именно наш код с ним взаимодействует;
  • не делаем ни одного реального HTTP‑запроса.

Мокирование через библиотеки (пример на Jest / Java / GoMock)

Иногда ручные моки становятся громоздкими. Специальные библиотеки упрощают создание моков и проверку ожиданий.

Пример на Jest (JavaScript / TypeScript)

Давайте посмотрим, как это реализовано в тесте для Node.js:

// service.js
// Здесь мы описываем модуль, который хотим протестировать
const mailer = require("./mailer")

async function registerUser(user) {
  // Здесь мы вызываем внешнюю зависимость - почтовый сервис
  await mailer.sendWelcome(user.email)
  return { status: "ok" }
}

module.exports = { registerUser }
// mailer.js
// Здесь определен реальный почтовый модуль
async function sendWelcome(email) {
  // Здесь могли бы быть реальные сетевые вызовы к почтовому сервису
  console.log("Send email to " + email)
}

module.exports = { sendWelcome }
// service.test.js
// Здесь мы пишем тест и мокируем модуль mailer
jest.mock("./mailer") // Здесь мы говорим Jest подменить настоящий mailer на мок

const mailer = require("./mailer")
const { registerUser } = require("./service")

test("registerUser calls mailer.sendWelcome", async () => {
  // Здесь мы задаем поведение мока - функция возвращает успешно завершенный промис
  mailer.sendWelcome.mockResolvedValueOnce()

  const user = { email: "user@example.com" }
  await registerUser(user)

  // Здесь мы проверяем, что sendWelcome был вызван ровно один раз
  expect(mailer.sendWelcome).toHaveBeenCalledTimes(1)
  // И что его вызывали с нужным аргументом
  expect(mailer.sendWelcome).toHaveBeenCalledWith("user@example.com")
})

Здесь библиотека:

  • автоматически создает мок вместо настоящего модуля;
  • предоставляет удобные функции toHaveBeenCalledWith, toHaveBeenCalledTimes;
  • избавляет вас от необходимости писать вспомогательный код вручную.

Пример на Java с Mockito

Еще один распространенный пример — Java + Mockito. Покажу вам, как это выглядит в тесте сервиса:

// UserService.java
// Здесь мы описываем сервис с зависимостью от репозитория
public class UserService {
    private final UserRepository repository;
    private final Mailer mailer;

    public UserService(UserRepository repository, Mailer mailer) {
        // Внедряем зависимости через конструктор
        this.repository = repository;
        this.mailer = mailer;
    }

    public void register(String email) {
        // Сохраняем пользователя
        User user = new User(email);
        repository.save(user);
        // Отправляем письмо
        mailer.sendWelcome(email);
    }
}
// UserServiceTest.java
// Здесь мы тестируем UserService с использованием Mockito
public class UserServiceTest {

    @Test
    public void register_sendsWelcomeEmail() {
        // Создаем моки для зависимостей
        UserRepository repository = Mockito.mock(UserRepository.class);
        Mailer mailer = Mockito.mock(Mailer.class);

        // Создаем тестируемый объект, передав ему моки
        UserService service = new UserService(repository, mailer);

        // Вызываем метод, который хотим протестировать
        service.register("user@example.com");

        // Проверяем, был ли вызван метод save у репозитория
        Mockito.verify(repository).save(Mockito.any(User.class));

        // Проверяем, что почтовый сервис вызывался с нужным email
        Mockito.verify(mailer).sendWelcome("user@example.com");
    }
}

Здесь библиотека Mockito:

  • создаёт динамические прокси‑объекты;
  • записывает информацию о вызовах;
  • позволяет удобно проверять ожидания с помощью verify.

Мокирование HTTP‑клиентов и внешних API

Очень частая задача — замокировать HTTP‑запросы. Здесь подходы зависят от языка:

  • В Go часто создают http.Client с кастомным RoundTripper.
  • В JS используют библиотеки вроде nock.
  • В Python — responses, requests-mock.

Покажу пример на Go с кастомным транспортом.

Мокирование HTTP‑клиента через RoundTripper

// MockTransport - реализует интерфейс http.RoundTripper
type MockTransport struct {
    // Здесь мы можем описать, какой ответ вернуть
    ResponseStatus int
    ResponseBody   string
    // И какую ошибку эмулировать, если нужно
    Err error

    // А также зафиксировать входящий запрос
    LastRequest *http.Request
}

func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    // Сохраняем запрос для последующей проверки
    m.LastRequest = req

    if m.Err != nil {
        // Если задана ошибка, возвращаем ее сразу
        return nil, m.Err
    }

    // Создаем фейковый HTTP-ответ
    body := io.NopCloser(strings.NewReader(m.ResponseBody)) // Здесь создаем тело ответа из строки

    resp := &http.Response{
        StatusCode: m.ResponseStatus,
        Body:       body,
        Header:     make(http.Header),
        Request:    req,
    }
    return resp, nil
}

Использование в тесте:

func TestClient_GetUser(t *testing.T) {
    // Здесь мы создаем транспорт, который будет подменять реальные HTTP-запросы
    mockTransport := &MockTransport{
        ResponseStatus: 200,                       // Эмулируем успешный статус
        ResponseBody:   `{"id":1,"name":"John"}`,  // Эмулируем JSON-ответ
    }

    // Создаем HTTP-клиент, который использует наш мок-транспорт
    httpClient := &http.Client{
        Transport: mockTransport,
    }

    // Передаем его в наш клиент, который мы хотим протестировать
    client := NewUserAPIClient(httpClient, "https://api.example.com")

    // Вызываем боевой код
    user, err := client.GetUser(1)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    if user.ID != 1 || user.Name != "John" {
        t.Errorf("unexpected user: %+v", user)
    }

    // Дополнительно проверяем, какой URL был вызван
    if mockTransport.LastRequest.URL.Path != "/users/1" {
        t.Errorf("unexpected path: %s", mockTransport.LastRequest.URL.Path)
    }
}

Как видите, этот код:

  • не делает ни одного реального HTTP‑запроса;
  • полностью контролирует статус и тело ответа;
  • позволяет проверить, какой именно запрос был сформирован.

Типичные ошибки при использовании моков

Избыточное мокирование

Одна из самых частых ошибок — мокировать всё подряд. Тогда ваши тесты:

  • становятся привязанными к внутренней структуре кода;
  • плохо отражают реальную работу системы;
  • ломаются при малейших рефакторах.

Рекомендация: мокируйте только реальные внешние зависимости и тяжёлые компоненты, а не каждый метод подряд.

Проверка лишних деталей реализации

Если вы в тестах:

  • проверяете каждый внутренний вызов;
  • завязываетесь на конкретную последовательность вызовов;

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

Лучше фокусироваться на:

  • публичном контракте метода (входные данные → выходные данные);
  • действительно важных взаимодействиях (вызвали ли внешнюю систему в принципиально необходимый момент).

Моки без интерфейсов и без инъекции зависимостей

Когда зависимости создаются прямо внутри кода, их сложно мокировать. Например:

func NewUserService() *UserService {
    // Здесь мы создаем http.Client прямо внутри - замокировать его будет тяжело
    client := &http.Client{}
    return &UserService{client: client}
}

Гораздо удобнее для тестирования сделать так:

func NewUserService(client *http.Client) *UserService {
    // Теперь мы можем в тесте передать сюда мок-клиент
    return &UserService{client: client}
}

То же касается других языков: используйте внедрение зависимостей (через конструктор, параметры или DI‑контейнер), чтобы замена на мок была естественной.

Практические паттерны мокирования

Паттерн «Порт и адаптер» (Hexagonal Architecture)

Если вы разделяете:

  • порты (интерфейсы вашей доменной логики — UserRepository, PaymentGateway);
  • адаптеры (конкретные реализации — PostgreSQL, Stripe, AWS SES);

то мокирование становится естественным: тесты работают с портами, а вы подставляете вместо реальных адаптеров моки или фейки.

Такой подход:

  • уменьшает связанность;
  • делает тесты проще и понятнее;
  • облегчает замену инфраструктуры.

Паттерн «Test Doubles per Interface»

Хорошая практика — для каждого важного интерфейса иметь:

  • фейковую in‑memory реализацию (для интеграционных/сложных тестов без реальной инфраструктуры);
  • один‑два специализированных мока (для проверки конкретных сценариев взаимодействия).

Не стоит плодить десятки разных моков для каждого теста — лучше иметь несколько хорошо продуманных и многократно используемых.

Моки для негативных сценариев

Особое внимание стоит уделять тестам, где:

  • внешний сервис недоступен;
  • база данных возвращает ошибку;
  • запрос занимает слишком много времени.

Моки позволяют вам легко воспроизвести такие сценарии. Вы можете, например:

  • возвращать специальные ошибки;
  • считать количество повторных попыток;
  • проверять, корректно ли срабатывает fallback‑логика.

Заключение

Мокирование данных — один из ключевых инструментов при написании автоматических тестов. Оно помогает изолировать код, убрать внешние зависимости и сделать тесты быстрыми и предсказуемыми. При этом важно:

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

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

Частозадаваемые технические вопросы по теме статьи и ответы на них

Как замокировать время (time.Now) в тестах

Вместо прямого вызова глобальной функции оберните время в интерфейс.

Пример на Go:

// Интерфейс для получения текущего времени
type Clock interface {
    Now() time.Time
}

// Реальная реализация - использует time.Now
type RealClock struct{}

func (RealClock) Now() time.Time {
    return time.Now()
}

// Мок-часы для теста
type FixedClock struct {
    Fixed time.Time
}

func (c FixedClock) Now() time.Time {
    // Всегда возвращаем одно и то же время
    return c.Fixed
}

В коде вместо time.Now используйте Clock, а в тесте передавайте FixedClock с нужной датой.


Как замокировать глобальные или статические функции

Глобальные зависимости сложно мокировать. Типичные варианты:

  • Вынести вызов глобальной функции в обертку‑интерфейс (как с временем выше).
  • Использовать DI и передавать зависимость как параметр/поле.
  • В некоторых языках (JS, Python) использовать возможности рантайма — подмену/монкипатчинг (jest.mock, monkeypatch и т.п.), но делать это аккуратно.

Общий подход — не обращаться к глобальной функции напрямую из доменной логики, а спрятать ее за абстракцией.


Как мокировать асинхронный код и события

При мокировании асинхронного кода важно:

  • управлять моментом завершения операции;
  • уметь проверять, что колбэки/обработчики были вызваны.

Подходы:

  • В JS — использовать async/await, промисы и Jest fake timers для контроля таймеров.
  • В других языках — возвращать из моков заранее завершенные или контролируемые future/promise/каналы.

Например, в тесте можно заранее подготовить promise, который вы завершите вручную, а затем проверите реакции кода.


Как замокировать базу данных если используется ORM

Здесь есть три основных пути:

  1. Вынести операции БД в интерфейсы репозиториев и мокировать репозитории (рекомендуется).
  2. Использовать in‑memory режим БД (SQLite in‑memory, H2 в Java) — это скорее фейк, чем мок.
  3. Использовать специальные библиотеки‑моки для конкретного ORM, которые перехватывают запросы (подход зависит от фреймворка).

Хорошая стратегия — доменная логика не должна знать о конкретном ORM, она должна работать с интерфейсами репозиториев.


Как тестировать код с ретраями и экспоненциальной задержкой

Основная проблема — реальные задержки делают тесты медленными. Решения:

  • Вынести стратегию ожидания (sleep/таймеры) в отдельный интерфейс и замокировать его — в тесте задержки можно делать нулевыми.
  • Использовать фейковые таймеры (fake timers) в тех фреймворках, где это поддерживается (Jest, некоторые библиотеки для Java и др.).
  • Проверять количество попыток через мок, фиксирующий число вызовов, а не реальное время.
Стрелочка влевоСостояние приложения - как устроен state-management и зачем он нуженРабота с API в Go Golang - api integration для разработчиковСтрелочка вправо

Все гайды по Fsd

Открыть базу знаний

Отправить комментарий