Введение
QSignalSpy — специализированный инструмент QTest для проверки факта и параметров испускания сигналов без необходимости вручную подключать слот-наблюдатель и хранить состояние. В предыдущей статье он упоминался для базовой проверки взаимодействий; здесь разберём более продвинутые сценарии — проверку нескольких сигналов, асинхронное ожидание сигналов с таймаутом и работу с сигналами, имеющими сложные типы аргументов.
Концепция
QSignalSpy подключается к конкретному сигналу конкретного объекта и накапливает список всех его испусканий с момента создания спая — каждое испускание сохраняется как QList<QVariant> с аргументами сигнала. Методы count() (или size()) показывают число испусканий, оператор [] даёт доступ к аргументам конкретного вызова. Для тестирования асинхронного кода (например, сигнала, который придёт через некоторое время после сетевого запроса) есть метод wait(timeout), который запускает локальный event loop и ждёт следующего испускания сигнала или истечения таймаута — это позволяет тестировать асинхронный код без искусственных QThread::sleep().
Пример кода
#include <QtTest/QtTest>
#include <QTimer>
#include <QSignalSpy>
class AsyncCounter : public QObject
{
Q_OBJECT
public:
void startCountingTo(int max)
{
m_current = 0;
m_max = max;
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, &AsyncCounter::tick);
m_timer->start(50);
}
signals:
void valueChanged(int value);
void finished();
private slots:
void tick()
{
m_current++;
emit valueChanged(m_current);
if (m_current >= m_max) {
m_timer->stop();
emit finished();
}
}
private:
QTimer *m_timer = nullptr;
int m_current = 0;
int m_max = 0;
};
class TestAsyncCounter : public QObject
{
Q_OBJECT
private slots:
void testValueChangedEmittedCorrectNumberOfTimes()
{
AsyncCounter counter;
QSignalSpy spy(&counter, &AsyncCounter::valueChanged);
counter.startCountingTo(5);
// Ждём сигнал finished с таймаутом — без блокирующего sleep
QSignalSpy finishedSpy(&counter, &AsyncCounter::finished);
QVERIFY(finishedSpy.wait(1000));
QCOMPARE(spy.count(), 5);
// Проверяем аргумент последнего испускания сигнала
QList<QVariant> lastEmission = spy.takeLast();
QCOMPARE(lastEmission.at(0).toInt(), 5);
}
void testIntermediateValues()
{
AsyncCounter counter;
QSignalSpy spy(&counter, &AsyncCounter::valueChanged);
counter.startCountingTo(3);
QSignalSpy finishedSpy(&counter, &AsyncCounter::finished);
QVERIFY(finishedSpy.wait(1000));
QCOMPARE(spy.count(), 3);
QCOMPARE(spy.at(0).at(0).toInt(), 1); // первое испускание: value == 1
QCOMPARE(spy.at(1).at(0).toInt(), 2); // второе испускание: value == 2
QCOMPARE(spy.at(2).at(0).toInt(), 3); // третье испускание: value == 3
}
};
QTEST_MAIN(TestAsyncCounter)
#include "test_asynccounter.moc"
Пояснения к коду
QSignalSpy spy(&counter, &AsyncCounter::valueChanged) начинает отслеживать испускания сигнала valueChanged с момента создания спая. После запуска асинхронного счётчика тест использует finishedSpy.wait(1000) — это блокирует тест не более чем на секунду, но возвращает управление сразу, как только сигнал finished будет испущен, не дожидаясь полного таймаута искусственно.
spy.at(i).at(0) обращается к первому аргументу i-го испускания сигнала — поскольку valueChanged(int value) имеет один аргумент, индекс аргумента всегда 0; для сигналов с несколькими аргументами они были бы доступны по индексам 0, 1, 2 и так далее в порядке объявления параметров.
Подводные камни
- Создание
QSignalSpyпосле того, как сигнал уже был испущен, не «ловит» прошлые испускания — спай должен быть создан до выполнения действия, которое вызовет интересующий сигнал, иначе тест будет ошибочно показыватьcount() == 0. wait()без указания таймаута использует значение по умолчанию (обычно 5000 мс), что может незаметно сделать тестовый набор медленным, если ожидаемый сигнал никогда не приходит (например, из-за бага) — каждый отдельный тест начинает «зависать» на полный таймаут, что особенно болезненно при множестве таких тестов в CI.- Использование
QSignalSpyдля проверки сигналов, которые могут быть испущены до создания спая в многопоточном коде — гонка между запуском фоновой операции и созданием спая может привести к ложным провалам тестов, зависящим от тайминга; стоит структурировать код так, чтобы спай создавался строго до старта асинхронной операции. - Сравнение
QVariantнапрямую без приведения к конкретному типу может давать неожиданные результаты при сравнении значений разных, но «похожих» типов (например,intиqlonglong) — явное приведение через.toInt(),.toString()и так далее, как показано в примере, делает сравнение предсказуемым.