Тестирование

Мокирование зависимостей в Qt-тестах: подходы и инструменты

Введение

Юнит-тест должен проверять конкретную единицу логики в изоляции, но реальный код часто зависит от других объектов — сетевых клиентов, баз данных, файловой системы. Подмена таких зависимостей «заглушками» (mock-объектами) позволяет тестировать логику класса без реального обращения к сети или диску. В Qt нет встроенного фреймворка мокирования, аналогичного gMock, но есть несколько практичных подходов, основанных на абстрактных интерфейсах и сигналах/слотах. В статье разберём их.

Концепция

Базовый подход — выделить зависимость за абстрактный интерфейс (чистый виртуальный класс или QObject с виртуальными методами) и внедрить конкретную реализацию через конструктор (dependency injection). В тестах вместо реальной реализации подставляется mock-класс, который либо возвращает заранее заданные значения, либо записывает вызовы для последующей проверки.

Для проверки взаимодействий через сигналы/слоты Qt предоставляет QSignalSpy — он не требует написания отдельного mock-класса и позволяет проверить, что сигнал был испущен нужное количество раз с нужными аргументами, что часто достаточно для тестирования взаимодействия объектов без полноценного мокирования.

Пример кода

// inetworkclient.h — абстракция сетевого клиента
#pragma once
#include <QString>

class INetworkClient
{
public:
    virtual ~INetworkClient() = default;
    virtual QString fetchUserName(int userId) = 0;
};

// usergreeter.h — класс, зависящий от INetworkClient
#pragma once
#include "inetworkclient.h"

class UserGreeter
{
public:
    explicit UserGreeter(INetworkClient *client) : m_client(client) {}

    QString greet(int userId)
    {
        const QString name = m_client->fetchUserName(userId);
        return name.isEmpty() ? "Привет, незнакомец!" : "Привет, " + name + "!";
    }

private:
    INetworkClient *m_client;
};
// test_usergreeter.cpp — мокирование зависимости вручную
#include <QtTest/QtTest>
#include "usergreeter.h"

class MockNetworkClient : public INetworkClient
{
public:
    QString fetchUserName(int userId) override
    {
        m_lastRequestedId = userId;
        m_callCount++;
        return m_nextReturnValue;
    }

    QString m_nextReturnValue;
    int m_lastRequestedId = -1;
    int m_callCount = 0;
};

class TestUserGreeter : public QObject
{
    Q_OBJECT
private slots:
    void testGreetWithKnownUser()
    {
        MockNetworkClient mockClient;
        mockClient.m_nextReturnValue = "Анна";

        UserGreeter greeter(&mockClient);
        QCOMPARE(greeter.greet(42), QString("Привет, Анна!"));
        QCOMPARE(mockClient.m_lastRequestedId, 42);
        QCOMPARE(mockClient.m_callCount, 1);
    }

    void testGreetWithEmptyName()
    {
        MockNetworkClient mockClient;
        mockClient.m_nextReturnValue = "";

        UserGreeter greeter(&mockClient);
        QCOMPARE(greeter.greet(1), QString("Привет, незнакомец!"));
    }
};

QTEST_APPLESS_MAIN(TestUserGreeter)
#include "test_usergreeter.moc"

Пояснения к коду

INetworkClient — абстрактный интерфейс, который UserGreeter использует через указатель, не зная о конкретной реализации. MockNetworkClient реализует этот интерфейс для тестов: вместо реального сетевого запроса он сразу возвращает заранее заданное значение m_nextReturnValue, а также запоминает аргумент последнего вызова и общее количество вызовов — это позволяет в тесте не только проверить результат работы UserGreeter, но и убедиться, что зависимость была вызвана корректно (с правильным userId, ровно один раз).

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

Подводные камни

  • Мокирование классов без выделенного интерфейса. Если зависимость — конкретный класс без абстракции (например, прямое использование QNetworkAccessManager внутри бизнес-логики), подменить её в тесте без рефакторинга невозможно — это сильный аргумент в пользу проектирования зависимостей через интерфейсы с самого начала, а не только «когда понадобится тестировать».
  • Слишком «умные» моки, повторяющие логику реальной реализации. Если mock-класс начинает содержать сложную условную логику, имитирующую поведение реального сервиса, тест начинает проверять корректность мока, а не реального кода — мок должен быть максимально простым, возвращающим заданные значения.
  • QSignalSpy не заменяет полноценный mock там, где важны входные параметры вызова, а не только факт сигнала. QSignalSpy хорошо подходит для проверки «сигнал испущен N раз с такими аргументами», но не подходит для тестирования зависимостей, взаимодействие с которыми происходит через обычные методы, а не сигналы.
  • Использование реальных синглтонов вместо инъекции зависимостей делает мокирование практически невозможным без глобальной подмены состояния синглтона между тестами, что плохо масштабируется и создаёт риск утечки состояния между тестовыми случаями.