How to test a QStateMachine?

2.7k views Asked by At

I'm a bit confused about how to test a QStateMachine. I have a project well organized with source code in one side and test code on the other side.

header

class Foo
{
    signals:
        void sigGoToStateOne();
        void sigGoToStateTwo();
        void sigGoToStateThree();

    private:
        QStateMachine *stateMachine;
        QState *state1;
        QState *state2;

        void initStateMachine();
}

And in the source file

Foo::initStateMachine()
{
    // constructors
    state1->addTransition(this,SIGNAL(sigGoToStateTwo()),this->state2);
    state2->addTransition(this,SIGNAL(sigGoToStateOne()),this->state1);
}

I would like to know if there is a beautiful way to test if my stateMachine is right. In other words, how my state machine reacts if I emit sigGoToStateThree() if I'm there, etc..

Solutions i see: 1 - Get the address of stateMachine (and eventually all other states) and test it (But i don't know how) 2 - Simulate signals (sigGoToStateX()) from a test file (Again, don't know if it's possible to emit signals of my class Foo in an other class)

My unique demand is I don't want to modify the core of my source file.

Thank's in advance.

2

There are 2 answers

0
Kuba hasn't forgotten Monica On BEST ANSWER

In Qt 5, signals are always public methods. To make your code compatible with Qt 4, you can make the signals explicitly public like so:

class Foo {
public:
  Q_SIGNAL void sigGoToStateOne();
  ...
}

Alternatively, you can keep arbitrary signal visibility, and declare a friend test class:

class Foo {
  friend class FooTest;
  ...
}

Finally, you can create a test project where you use the Qt's test framework to test the Foo class's behavior. The code below works in both Qt 4 and Qt 5.

// main.cpp
#include <QCoreApplication>
#include <QStateMachine>
#include <QEventLoop>
#include <QtTest>
#include <QTimer>

class Waiter {
   QTimer m_timer;
public:
   Waiter() {}
   Waiter(QObject * obj, const char * signal) {
      m_timer.connect(obj, signal, SIGNAL(timeout()));
   }
   void stop() {
      m_timer.stop();
      QMetaObject::invokeMethod(&m_timer, "timeout");
   }
   void wait(int timeout = 5000) {
      QEventLoop loop;
      m_timer.start(timeout);
      loop.connect(&m_timer, SIGNAL(timeout()), SLOT(quit()));
      loop.exec();
   }
};

class SignalWaiter : public QObject, public Waiter {
   Q_OBJECT
   int m_count;
   Q_SLOT void triggered() {
      ++ m_count;
      stop();
   }
public:
   SignalWaiter(QObject * obj, const char * signal) : m_count(0) {
      connect(obj, signal, SLOT(triggered()), Qt::QueuedConnection);
   }
   int count() const { return m_count; }
};

#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
typedef QSignalSpy SignalSpy;
#else
class SignalSpy : public QSignalSpy, public Waiter {
public:
   SignalSpy(QObject * obj, const char * signal) :
      QSignalSpy(obj, signal), Waiter(obj, signal) {}
};
#endif

class Foo : public QObject {
   Q_OBJECT
   friend class FooTest;
   QStateMachine m_stateMachine;
   QState m_state1;
   QState m_state2;
   Q_SIGNAL void sigGoToStateOne();
   Q_SIGNAL void sigGoToStateTwo();
public:
   explicit Foo(QObject * parent = 0) :
      QObject(parent),
      m_state1(&m_stateMachine),
      m_state2(&m_stateMachine)
   {
      m_stateMachine.setInitialState(&m_state1);
      m_state1.addTransition(this, SIGNAL(sigGoToStateTwo()), &m_state2);
      m_state2.addTransition(this, SIGNAL(sigGoToStateOne()), &m_state1);
   }
   Q_SLOT void start() {
      m_stateMachine.start();
   }
};

class FooTest : public QObject {
   Q_OBJECT
   void call(QObject * obj, const char * method) {
      QMetaObject::invokeMethod(obj, method, Qt::QueuedConnection);
   }
   Q_SLOT void test1() {
      // Uses QSignalSpy
      Foo foo;
      SignalSpy state1(&foo.m_state1, SIGNAL(entered()));
      SignalSpy state2(&foo.m_state2, SIGNAL(entered()));
      call(&foo, "start");
      state1.wait();
      QCOMPARE(state1.count(), 1);
      call(&foo, "sigGoToStateTwo");
      state2.wait();
      QCOMPARE(state2.count(), 1);
      call(&foo, "sigGoToStateOne");
      state1.wait();
      QCOMPARE(state1.count(), 2);
   }

   Q_SLOT void test2() {
      // Uses SignalWaiter
      Foo foo;
      SignalWaiter state1(&foo.m_state1, SIGNAL(entered()));
      SignalWaiter state2(&foo.m_state2, SIGNAL(entered()));
      foo.start();
      state1.wait();
      QCOMPARE(state1.count(), 1);
      emit foo.sigGoToStateTwo();
      state2.wait();
      QCOMPARE(state2.count(), 1);
      emit foo.sigGoToStateOne();
      state1.wait();
      QCOMPARE(state1.count(), 2);
   }
};

int main(int argc, char *argv[])
{
   QCoreApplication a(argc, argv);
   FooTest test;
   QTest::qExec(&test, a.arguments());
   QMetaObject::invokeMethod(&a, "quit", Qt::QueuedConnection);
   return a.exec();
}

#include "main.moc"

I am forcing all signal invocations to be done from the event loop, so that the event transitions will only happen while the event loop is running. This makes the test code uniformly wait after each transition. Otherwise, the second wait would time out:

Q_SLOT void test1() {
   SignalSpy state1(&m_foo.m_state1, SIGNAL(entered()));
   SignalSpy state2(&m_foo.m_state2, SIGNAL(entered()));
   m_foo.start();
   state1.wait();
   QCOMPARE(state1.count(), 1);
   emit m_foo.sigGoToStateTwo(); // The state2.entered() signal is emitted here.
   state2.wait(); // But we wait for it here, and this wait will time out.
   QCOMPARE(state2.count(), 1); // But of course the count will match.
   emit m_foo.sigGoToStateOne();
   state1.wait(); // This would timeout as well.
   QCOMPARE(state1.count(), 2);
}

This can be worked around without the use of explicit queued calls by the use of a signal spy class that internally uses a queued connection.

2
dannyp On

Kuba Ober gives a very good analysis of how to use the test framework & SignalSpy to do in depth testing of your state machine.

If all you're trying to do is generate a sigGoToStateX() from a test file then don't forget that you can chain signals together.

So for example given a class "Tester":

class Tester : public QObject {
Q_OBJECT
public:
    Tester(Foo *fooClass) {
        //Connecting signals gives you the kind of behaviour you were asking about
        connect(this, SIGNAL(testTransitionToState1()), fooClass, SIGNAL(sigGoToState1()));
        connect(this, SIGNAL(testTransitionToState2()), fooClass, SIGNAL(sigGoToState2()));
        connect(this, SIGNAL(testTransitionToState3()), fooClass, SIGNAL(sigGoToState3()));
    }

    void SwitchState(int newState) {
        //Now any time we emit the test signals, the foo class's signals will be emitted too!
        if (newState == 1) emit testTransitionToState1();
        else if (newState == 2) emit testTransitionToState1();
        else if (newState == 3) emit testTransitionToState1();
    }

signals:
    void testTransitionToState1();
    void testTransitionToState2();
    void testTransitionToState3();
}

So for example calling SwitchState(1) will invoke the correct signals for switching to state 1. If this simple case is all you need for testing then that's all you really need.

If you need something more complex, go with the full SignalSpy example.