【问题标题】:How to test a QStateMachine?如何测试 QStateMachine?
【发布时间】:2014-06-11 09:01:55
【问题描述】:

我对如何测试 QStateMachine 有点困惑。 我有一个项目组织良好,一侧是源代码,另一侧是测试代码。

标题

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

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

        void initStateMachine();
}

在源文件中

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

我想知道是否有一种很好的方法来测试我的 stateMachine 是否正确。换句话说,如果我在那里发出 sigGoToStateThree(),我的状态机将如何反应,等等。

我看到的解决方案: 1 - 获取 stateMachine 的地址(以及最终所有其他状态)并测试它(但我不知道如何) 2 - 从测试文件模拟信号(sigGoToStateX())(再次,不知道是否可以在其他类中发出我的类 Foo 的信号)

我的独特需求是我不想修改我的源文件的核心。

提前致谢。

【问题讨论】:

  • Re 2:信号是常规的 C++ 方法。 “发射”它们只是调用它们的方法。从字面上看,这没什么。

标签: qt qstatemachine


【解决方案1】:

在 Qt 5 中,信号始终是公共方法。为了使您的代码与 Qt 4 兼容,您可以像这样显式公开信号:

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

或者,您可以保持任意信号可见性,并声明一个友元测试类:

class Foo {
  friend class FooTest;
  ...
}

最后,您可以创建一个测试项目,在其中使用 Qt 的测试框架来测试 Foo 类的行为。下面的代码适用于 Qt 4 和 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"

我强制所有信号调用从事件循环中完成,这样事件转换只会在事件循环运行时发生。这使得测试代码在每次转换后统一等待。否则,第二个wait 会超时:

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

通过使用内部使用排队连接的信号间谍类,可以在不使用显式排队调用的情况下解决此问题。

【讨论】:

    【解决方案2】:

    Kuba Ober 对如何使用测试框架和 SignalSpy 对状态机进行深入测试进行了很好的分析。

    如果您要做的只是从测试文件生成一个 sigGoToStateX(),那么不要忘记您可以将信号链接在一起。

    例如,给定一个类“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();
    }
    

    因此,例如调用 SwitchState(1) 将调用正确的信号以切换到状态 1。如果您只需要这种简单的情况进行测试,那么这就是您真正需要的。

    如果您需要更复杂的东西,请使用完整的 SignalSpy 示例。

    【讨论】:

    • 信号是完全常规的 C++ 方法。根本没有理由经历这些间接循环。将emit testTransitionToState1 替换为emit foo-&gt;sigGoToState1。如果您的目标是 Qt 4(公共部分中的Q_SIGNAL),请确保您的信号是公开的。这仍然没有显示如何实际 test 任何东西。你所做的就是调用信号。那是微不足道的。你还没有展示如何,你知道,确保信号任何事情。
    • 是的,但我假设他正在尝试基于我们现阶段不知道的一些任意外部架构来调用信号,所以这个例子只是为了证明这与信号/插槽连接,仅使用信号。我完全同意这是一件微不足道的事情,并且没有回答如何测试内部状态的问题,但提出的问题之一是如何从更广泛的架构角度调用 foo 类外部的信号。跨度>
    猜你喜欢
    • 1970-01-01
    • 2016-10-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多