Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Сурово-типизированный SignalSpy для тестирования Qt приложений

Anna | 24.06.2014 | нет комментариев
При написании юнит-тестов за правило классного тона считается проверка инвариантов класса посредством открытого интерфейса класса. В случае с 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<QVariant> firstCallArgs = spy.at(0); 
QList<QVariant> secondCallArgs = spy.at(1);

Как видно из последних 2-х строк, сам QSignalSpy наследует от QList<QList<QVariant> > (у здорового человека тут должен прозвенеть синтактический звоночек), где внутренний QList хранит значения посланные с сигналом за определённый вызов, а внешний — ведёт протокол самих вызовов.

В приведенном примере ожидвается следующее:

assert(2 == firstCallArgs.size());
assert(58 == firstCallArgs.at(0).toInt()); // 2-й синтактический звоночек
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)) каждого-лишь создаёт из сигнатуры сигнала строковую константу (3-й звоночек).
  • если в ходе теста, Вам потребоваться проверить, что сигнал ни разу не был вызван, т.е
    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<SomeClass, int> особенно не хочется, по-этому давайте, между делом, сделаем фабрику:

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)) );

Другое дело.

 

Источник: programmingmaster.ru

 

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB