[Из песочницы] Строго-типизированный SignalSpy для тестирования Qt приложений

При написании юнит-тестов за правило хорошего тона считается проверка инвариантов класса посредством открытого интерфейса класса. В случае с Qt всё немного сложнее, так как функции-члены могут потенциально посылать сигналы, которые выходят «наружу» объектов и являются тем самым частью открытого интерфейса. Для этих целей в модуле QtTestLib имеется полезный класс QSignalSpy, который следит за определённым сигналом издаваемым тестируемым обьектом и скурпулёзно ведёт протокол, сколько раз и с какими значениями этот сигнал был вызван.Вот как это работает:. // Предполагается, что в классе MyClass определён сигнал «void someSignal (int, bool)». MyClass someObject; QSignalSpy spy (&someObject, SIGNAL (someSignal (int, bool))); // шпионим за сигналом «someSignal».

emit someObject.someSignal (58, true); emit someObject.someSignal (42, false); QList firstCallArgs = spy.at (0); QList secondCallArgs = spy.at (1); Как видно из последних двух строк, сам QSignalSpy наследует от QList > (у здорового человека здесь должен прозвенеть синтактический звоночек), где внутренний QList хранит значения посланные с сигналом за определённый вызов, а внешний — ведёт протокол самих вызовов.В приведенном примере ожидвается следующее:

assert (2 == firstCallArgs.size ()); assert (58 == firstCallArgs.at (0).toInt ()); // второй синтактический звоночек assert (true == firstCallArgs.at (0).toBool ());

assert (2 == secondCallArgs.size ()); assert (42 == secondCallArgs.at (1).toInt ()); assert (false == secondCallArgs.at (2).toBool ()); Как Вы видите, у данного подхода есть ряд недостатков: если кто-то переименует сигнал someSignal, код по прежнему будет компилироваться, так как запись SIGNAL (someSignal (int, bool)) всего-лишь создаёт из сигнатуры сигнала строковую константу (третий звоночек). если в ходе теста, Вам понадобиться проверить, что сигнал ни разу не был вызван, т.еassert (0 == spy.size ()); то в случае переименовывания сигнала, тест будет не только компилироваться, но ещё и успешно проходить выполнение. все распаковки из QVariant в …toInt (), …toBool () и так далее компилируются в независимости от того, что изначально было в этот QVariant запаковано. В крайнем случае получите 0. А если Вы как раз хотите проверить значение на равенство нулю, то Ваш тест будет работать даже после того как кто-то поменяет аргумент сигнала с int на QString. ну, и последнее: необходимость всё время распаковывать содержимое QVariant’а немного утомляет. Если подобные недостатки вызывают у Вас недовольство и если Вы относитесь к тем программистам, которые пишут машинный код медленнее и хуже компилятора, то давайте попросим компилятор заодно и помочь в решении проблемы со шпионом сигналов.Итак, что же нужно сделать? Для начала, набросаем шапку класса:

template <... тут потом заполним...> class SignalSpy; Определять обьекты нашего класса хотелось используя не строку с именем сигнала, а сам сигнал. Т.е как-то так: SignalSpy<...тут чё-то...> spy (&someObject, &MyClass: someSignal); Так мы сможем узнать на этапе компиляции, что такого сигнала, к примеру, нет или, что список его аргументов не подходит под тип шпиона.Далее, зачем хранить аргументы вызова в списке QVariant’ов, если их количество и качество известно заранее? Намного лучше было бы использовать что-то вроде такого чудища:

std: tuple А протокол вызовов будет выглядеть так: QList > Наследовать от этого чудовища не обязательно, так что давайте просто отложим всю эту структуру в виде открытого аттрибута класса SignalSpy. Подведём промежуточные итоги. Имеем: template <...тут потом заполним...> class SignalSpy { public: // Конструктор SignalSpy (T* signalSource, …какой-то сигнал класса Т) { … как-то сделать так, чтобы, когда вызывался сигнал, все его аргументы записывались в m_calls… } QList > m_calls; }; Пришла пора заполнять троеточия. Если предположить, что мы хотим ограничить полученный класс на перехват сигналов с только одним аргументом, то можно сделать так: template class SignalSpy { public: SignalSpy (T* signalSource, void (T::*Method)(ArgT)); // параметр Method указывает на сигнал. std: list > m_calls; }; Вот как бы это выглядело в коде клиента: //класс SomeClass определяет сигнал void someSignal (int); SomeClass myObject; SignalSpy spy = SignalSpy(&myObject, &SomeClass: someSignal); Писать каждый раз нечто вроде SignalSpy особо не хочется, по-этому давайте, между делом, сделаем фабрику: template SignalSpy createSignalSpy (T* signalSource, void (T::*Method)(ArgT)) { return SignalSpy(signalSource, Method); }; Теперь можно определять шпионов так: auto spy = createSignalSpy (&signalSource, &SignalClass: someSignal); Уже лучше. Теперь давайте подумаем, как определить конструктор. Первое, что приходит в голову — lambda функция: SignalSpy (T* signalSource, void (T::*Method)(ArgT)); { QObject: connect (signalSource, Method, [this](ArgT arg) { // заносим аргументы сигнала в протокол. m_calls.push_back (std: make_tuple (arg)); }); } Ну, собственно, и всё. Осталось только обобщить для произвольного количества аргументов. Для этого нужно всего-лишь добавить несколько троеточий в определении шаблонов: template class SignalSpy { public: SignalSpy (T* signalSource, void (T::*Method)(ParamT…)) { QObject: connect (signalSource, Method, [this](ParamT… args) { m_calls.push_back (std: make_tuple (args…)); }); } QList > m_calls; }; // Ну и фабрика заодно template SignalSpy createSignalSpy (T* signalSource, void (Type::*Method)(ParamT…)) { return SignalSpy(signalSource, Method); }; Теперь можно с лёгкой душой писать строго-типизированный тест: auto spy = createSignalSpy (&signalSource, &SignalClass: someSignal); emit someObject.someSignal (58, true) emit someObject.someSignal (42, false);

assert (58, get<0>(spy.at (0))); assert (true, get<1>(spy.at (0))); assert (42, get<0>(spy.at (1))); assert (false, get<1>(spy.at (1))); Другое дело.

© Habrahabr.ru