Введение
Юнит-тест должен проверять конкретную единицу логики в изоляции, но реальный код часто зависит от других объектов — сетевых клиентов, баз данных, файловой системы. Подмена таких зависимостей «заглушками» (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 раз с такими аргументами», но не подходит для тестирования зависимостей, взаимодействие с которыми происходит через обычные методы, а не сигналы. - Использование реальных синглтонов вместо инъекции зависимостей делает мокирование практически невозможным без глобальной подмены состояния синглтона между тестами, что плохо масштабируется и создаёт риск утечки состояния между тестовыми случаями.