При написании юнит-тестов за правило хорошего тона считается проверка инвариантов класса посредством открытого интерфейса класса. В случае с Qt всё немного сложнее, так как функции-члены могут потенциально посылать сигналы, которые выходят «наружу» объектов и являются тем самым частью открытого интерфейса. Для этих целей в модуле QtTestLib имеется полезный класс
Вот как это работает:.
Как видно из последних двух строк, сам QSignalSpy наследует от
Как Вы видите, у данного подхода есть ряд недостатков:
Если подобные недостатки вызывают у Вас недовольство и если Вы относитесь к тем программистам, которые пишут машинный код медленнее и хуже компилятора, то давайте попросим компилятор заодно и помочь в решении проблемы со шпионом сигналов.
Определять обьекты нашего класса хотелось используя не строку с именем сигнала, а сам сигнал. Т.е как-то так:
Так мы сможем узнать на этапе компиляции, что такого сигнала, к примеру, нет или, что список его аргументов не подходит под тип шпиона.
А протокол вызовов будет выглядеть так:
Наследовать от этого чудовища не обязательно, так что давайте просто отложим всю эту структуру в виде открытого аттрибута класса SignalSpy. Подведём промежуточные итоги. Имеем:
Пришла пора заполнять троеточия. Если предположить, что мы хотим ограничить полученный класс на перехват сигналов с только одним аргументом, то можно сделать так:
Вот как бы это выглядело в коде клиента:
Писать каждый раз нечто вроде SignalSpy особо не хочется, по-этому давайте, между делом, сделаем фабрику:
Теперь можно определять шпионов так:
Уже лучше. Теперь давайте подумаем, как определить конструктор. Первое, что приходит в голову — lambda функция:
Ну, собственно, и всё. Осталось только обобщить для произвольного количества аргументов. Для этого нужно всего-лишь добавить несколько троеточий в определении шаблонов:
Теперь можно с лёгкой душой писать строго-типизированный тест:
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<QVariant> firstCallArgs = spy.at(0);
QList<QVariant> secondCallArgs = spy.at(1);
Как видно из последних двух строк, сам QSignalSpy наследует от
QList<QList<QVariant> >
(у здорового человека здесь должен прозвенеть синтактический звоночек), где внутренний 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<FirstArgT, SecondArgT,..., LastArgT>
А протокол вызовов будет выглядеть так:
QList<std::tuple<FirstArgT, SecondArgT,..., LastArgT> >
Наследовать от этого чудовища не обязательно, так что давайте просто отложим всю эту структуру в виде открытого аттрибута класса SignalSpy. Подведём промежуточные итоги. Имеем:
template <...тут потом заполним...>
class SignalSpy
{
public:
// Конструктор
SignalSpy(T* signalSource, ...какой-то сигнал класса Т)
{
… как-то сделать так, чтобы, когда вызывался
сигнал, все его аргументы записывались в m_calls...
}
QList<std::tuple<...потом заполним...> > m_calls;
};
Пришла пора заполнять троеточия. Если предположить, что мы хотим ограничить полученный класс на перехват сигналов с только одним аргументом, то можно сделать так:
template<typename T, typename ArgT>
class SignalSpy
{
public :
SignalSpy(T* signalSource, void (T::*Method)(ArgT)); // параметр Method указывает на сигнал.
std::list<std::tuple<ArgT> > m_calls;
};
Вот как бы это выглядело в коде клиента:
//класс SomeClass определяет сигнал void someSignal(int);
SomeClass myObject;
SignalSpy<SomeClass, int> spy = SignalSpy<SomeClass, int>(&myObject, &SomeClass::someSignal);
Писать каждый раз нечто вроде SignalSpy особо не хочется, по-этому давайте, между делом, сделаем фабрику:
template<typename T, typename ArgT >
SignalSpy<T, ArgT> createSignalSpy(T* signalSource, void (T::*Method)(ArgT))
{
return SignalSpy<T, ArgT>(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<typename T, typename... ParamT>
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<std::tuple<ParamT...> > m_calls;
};
// Ну и фабрика заодно
template<typename T,typename... ParamT>
SignalSpy<T, ParamT...> createSignalSpy(T* signalSource, void (Type::*Method)(ParamT...))
{
return SignalSpy<T, ParamT...>(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)) );
Другое дело.
This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.
Комментариев нет:
Отправить комментарий