【问题标题】:What unit-testing framework should I use for Qt? [closed]我应该为 Qt 使用什么单元测试框架? [关闭]
【发布时间】:2010-12-04 04:59:57
【问题描述】:

我刚刚开始一个需要跨平台 GUI 的新项目,我们选择 Qt 作为 GUI 框架。

我们也需要一个单元测试框架。直到大约一年前,我们还在为 C++ 项目使用内部开发的单元测试框架,但现在我们正在过渡到将 Google Test 用于新项目。

有人有使用 Google Test for Qt 应用程序的经验吗? QtTest/QTestLib 是更好的选择吗?

我仍然不确定我们想在项目的非 GUI 部分中使用多少 Qt - 我们可能更愿意在核心代码中使用 STL/Boost 以及与基于 Qt 的 GUI 的小接口.

编辑: 看起来很多人倾向于 QtTest。有没有人有将它与持续集成服务器集成的经验?此外,在我看来,必须为每个新的测试用例处理一个单独的应用程序会导致很多摩擦。有什么好的方法可以解决吗? Qt Creator 是否有处理此类测试用例的好方法,或者您是否需要为每个测试用例创建一个项目?

【问题讨论】:

  • 很高兴能有关于这个主题的更新(2021 年)。似乎 QSignalSpy 仍然可用doc-snapshots.qt.io/qt6-dev/qsignalspy.html Qtest 中的模拟支持如何,我最关心的是,因此我宁愿使用 Gtest。

标签: unit-testing qt googletest qtestlib qttest


【解决方案1】:

您不必创建单独的测试应用程序。只需在与此类似的独立 main() 函数中使用 qExec:

int main(int argc, char *argv[])
{
    TestClass1 test1;
    QTest::qExec(&test1, argc, argv);

    TestClass2 test2;
    QTest::qExec(&test2, argc, argv);

    // ...

    return 0;
}

这将在一个批次中执行每个类中的所有测试方法。

您的 testclass .h 文件如下所示:

class TestClass1 : public QObject
{
Q_OBJECT

private slots:
    void testMethod1();
    // ...
}

不幸的是,这个设置在 Qt 文档中并没有得到很好的描述,尽管它似乎对很多人来说非常有用。

【讨论】:

  • 我喜欢这种方法,但我收到错误Unknown test function: 'test()'. Possible matches: firstTest(),有什么提示吗?
  • qExec 不应被多次调用:因为它破坏了命令行支持。当您使用 QtTest 编写测试时,您可以使用 -functions 选项列出测试的所有函数,并通过在命令行中传递它来运行单个函数。如果 qExec 被多次调用,则会中断,因为只有第一次调用会处理命令行选项。
【解决方案2】:

我开始在我的应用程序中使用 QtTest,并且很快就遇到了限制。两个主要问题是:

1) 我的测试运行得非常快——足够快,以至于加载可执行文件、设置 Q(Core)Application(如果需要)等的开销通常会使测试本身的运行时间相形见绌!链接每个可执行文件也需要大量时间。

随着添加的类越来越多,开销不断增加,很快就变成了一个问题——单元测试的目标之一是拥有一个运行得如此之快的安全网,以至于它根本不是负担,情况很快就不是这样了。解决方案是将多个测试套件集成到一个可执行文件中,虽然(如上所示)这在很大程度上是可行的,但它是 not supported 并且有很大的局限性。

2) 没有固定装置支持 - 对我来说是个交易破坏者。

所以过了一段时间,我切换到了 Google Test - 它是一个功能更丰富、更复杂的单元测试框架(尤其是与 Google Mock 一起使用时)并解决了 1) 和 2),此外,您仍然可以轻松地使用方便的 QTestLib 功能,例如 QSignalSpy 和 GUI 事件的模拟等。切换起来有点痛苦,但幸运的是该项目没有进展得太远,并且许多更改可以自动化。

就我个人而言,我不会在未来的项目中使用 QtTest 而不是 Google Test - 如果它没有提供我可以看到的真正优势,并且有重要的缺点。

【讨论】:

  • 对于如何结合 Qt 和 gtest 有什么建议吗? IE:你还有 QApplication 或 QMainWindow 吗?您是直接将测试嵌入到 main 中,还是嵌入到 QObject 的某个后代的成员函数中?
  • @KeyserSoze 任何需要 QWidgets 的端到端/集成测试都有一个 QApplication;这个 QApplication 在我 RUN_ALL_TESTS() 之前在 main() 中创建(但不是 exec()'d)。 QMainWindow 可以使用,但我主要只在端到端测试中使用。测试本身遵循标准的谷歌测试方案,我通常有例如在一个名为 Xtests.cpp 的文件中对 X 类进行所有单元测试。所以它本质上是一个标准的 gtest 项目,对 Qt 做了一些让步(在以正常方式运行测试之前创建一个 QApplication)。
  • 我不知道它是否是最近的,但 Qt 测试允许添加两个私有插槽 initcleanup 分别在每个测试函数之前和之后调用。
【解决方案3】:

附加到乔的答案。

这是我使用的一个小头文件 (testrunner.h),其中包含一个产生事件循环的实用程序类(例如,需要测试排队的信号槽连接和数据库)和“运行”QTest 兼容类:

#ifndef TESTRUNNER_H
#define TESTRUNNER_H

#include <QList>
#include <QTimer>
#include <QCoreApplication>
#include <QtTest>

class TestRunner: public QObject
{
    Q_OBJECT

public:
    TestRunner()
        : m_overallResult(0)
    {}

    void addTest(QObject * test) {
        test->setParent(this);
        m_tests.append(test);
    }

    bool runTests() {
        int argc =0;
        char * argv[] = {0};
        QCoreApplication app(argc, argv);
        QTimer::singleShot(0, this, SLOT(run()) );
        app.exec();

        return m_overallResult == 0;
    }
private slots:
    void run() {
        doRunTests();
        QCoreApplication::instance()->quit();
    }
private:
    void doRunTests() {
        foreach (QObject * test, m_tests) {
            m_overallResult|= QTest::qExec(test);
        }
    }

    QList<QObject *> m_tests;
    int m_overallResult;
};

#endif // TESTRUNNER_H

像这样使用它:

#include "testrunner.h"
#include "..." // header for your QTest compatible class here

#include <QDebug>

int main() {
    TestRunner testRunner;
    testRunner.addTest(new ...()); //your QTest compatible class here

    qDebug() << "Overall result: " << (testRunner.runTests()?"PASS":"FAIL");

    return 0;
}

【讨论】:

  • 干得好!应该是 QCoreApplication 再次在私人插槽运行...
  • 谢谢,已修复!尚未测试,但很快就会测试(因为我在需要测试时使用此代码的变体:))
  • ... 一个问题:您使用QTimer 而不是QMetaObject::invokeMethodQt::QueuedConnection 有什么原因吗?我认为这会更清晰恕我直言。
  • @Terrabits 如果对象将与 QTimer 相同的线程具有亲和力(应该​​是 GUI 线程,因为可能禁止在外部创建 qApp-s),为什么要对连接进行排队? :)
  • 我相信无论哪种方式,槽都会排队进入主事件循环,在这种情况下,显式排队更直接(且清晰)。
【解决方案4】:

我不知道 QTestLib 在这种笼统的意义上比另一个框架“更好”。有一件事它做得很好,那就是提供了一种测试基于 Qt 的应用程序的好方法。

您可以将 QTest 集成到基于 Google 测试的新设置中。没试过,不过从QTestLib的架构来看,好像不会太复杂。

使用纯 QTestLib 编写的测试有一个可以使用的 -xml 选项,以及一些 XSLT 转换以转换为持续集成服务器所需的格式。但是,这在很大程度上取决于您使用的 CI 服务器。我想这同样适用于 GTest。

每个测试用例一个单独的测试应用程序从来没有给我造成太大的摩擦,但这取决于是否有一个构建系统能够很好地管理测试用例的构建和执行。

我不知道 Qt Creator 中有什么东西需要每个测试用例单独的项目,但自从我上次查看 Qt Creator 以来它可能已经改变了。

我还建议坚持使用 QtCore 并远离 STL。在整个过程中使用 QtCore 将使处理需要 Qt 数据类型的 GUI 位更容易。在这种情况下,您不必担心从一种数据类型转换为另一种数据类型。

【讨论】:

  • 我只想为后人指出,自最初的问题发布以来的 8 年多来发生了很大变化。在 2017 年,选择 googletest/mock 而不是 QTestLib 似乎是比以前更可行的选择。作为附件 A,这是来自 ICS 的名为 Qt Test-Driven Development Using Google Test & Google Mock 的网络研讨会。对我来说,使用 googletest/mock 和 QSignalSpy 真的很有效,我看不出有什么理由回去。
  • 嘿,所以我一直在寻找 QT 中的单元测试,但给定链接上的 vod 对我不起作用,或者它已被删除。您是否有任何可用的资源,我可以通过任何方式来大致了解如何开始?
【解决方案5】:

为什么不使用 Qt 中包含的单元测试框架? 一个例子:QtTestLib Tutorial.

【讨论】:

  • “QtTest/QTestLib 是更好的选择吗?” ...我认为这就是问题所在:-P
  • 链接已失效。这就是为什么只有链接的答案不好。
  • 链接已修复为标准 QT 文档网站,而不是以前的诺基亚网站
【解决方案6】:

我使用 gtest 和 QSignalSpy 对我们的库进行了单元测试。使用 QSignalSpy 捕捉信号。您可以直接调用槽(像普通方法一样)来测试它们。

【讨论】:

    【解决方案7】:

    QtTest 主要用于测试需要 Qt 事件循环/信号调度的部分。它的设计方式是每个测试用例都需要一个单独的可执行文件,因此它不应与用于应用程序其余部分的任何现有测试框架冲突。

    (顺便说一句,我强烈推荐使用 QtCore,即使是应用程序的非 GUI 部分。使用起来会更好。)

    【讨论】:

      【解决方案8】:

      为了扩展 mlvljr 和 Joe 的解决方案,我们甚至可以支持每个测试类的完整 QtTest 选项,并且仍然在批处理中运行所有内容并进行日志记录:

      usage: 
        help:                                        "TestSuite.exe -help"
        run all test classes (with logging):         "TestSuite.exe"
        print all test classes:                      "TestSuite.exe -classes"
        run one test class with QtTest parameters:   "TestSuite.exe testClass [options] [testfunctions[:testdata]]...
      

      标题

      #ifndef TESTRUNNER_H
      #define TESTRUNNER_H
      
      #include <QList>
      #include <QTimer>
      #include <QCoreApplication>
      #include <QtTest>
      #include <QStringBuilder>
      
      /*
      Taken from https://stackoverflow.com/questions/1524390/what-unit-testing-framework-should-i-use-for-qt
      BEWARE: there are some concerns doing so, see  https://bugreports.qt.io/browse/QTBUG-23067
      */
      class TestRunner : public QObject
      {
         Q_OBJECT
      
      public:
         TestRunner() : m_overallResult(0) 
         {
            QDir dir;
            if (!dir.exists(mTestLogFolder))
            {
               if (!dir.mkdir(mTestLogFolder))
                  qFatal("Cannot create folder %s", mTestLogFolder);
            }
         }
      
         void addTest(QObject * test)
         {
            test->setParent(this);
            m_tests.append(test);
         }
      
         bool runTests(int argc, char * argv[]) 
         {
            QCoreApplication app(argc, argv);
            QTimer::singleShot(0, this, SLOT(run()));
            app.exec();
      
            return m_overallResult == 0;
         }
      
         private slots:
         void run() 
         {
            doRunTests();
            QCoreApplication::instance()->quit();
         }
      
      private:
         void doRunTests() 
         {
            // BEWARE: we assume either no command line parameters or evaluate first parameter ourselves
            // usage: 
            //    help:                                        "TestSuite.exe -help"
            //    run all test classes (with logging):         "TestSuite.exe"
            //    print all test classes:                      "TestSuite.exe -classes"
            //    run one test class with QtTest parameters:   "TestSuite.exe testClass [options] [testfunctions[:testdata]]...
            if (QCoreApplication::arguments().size() > 1 && QCoreApplication::arguments()[1] == "-help")
            {
               qDebug() << "Usage:";
               qDebug().noquote() << "run all test classes (with logging):\t\t" << qAppName();
               qDebug().noquote() << "print all test classes:\t\t\t\t" << qAppName() << "-classes";
               qDebug().noquote() << "run one test class with QtTest parameters:\t" << qAppName() << "testClass [options][testfunctions[:testdata]]...";
               qDebug().noquote() << "get more help for running one test class:\t" << qAppName() << "testClass -help";
               exit(0);
            }
      
            foreach(QObject * test, m_tests)
            {
               QStringList arguments;
               QString testName = test->metaObject()->className();
      
               if (QCoreApplication::arguments().size() > 1)
               {
                  if (QCoreApplication::arguments()[1] == "-classes")
                  {
                     // only print test classes
                     qDebug().noquote() << testName;
                     continue;
                  }
                  else
                     if (QCoreApplication::arguments()[1] != testName)
                     {
                        continue;
                     }
                     else
                     {
                        arguments = QCoreApplication::arguments();
                        arguments.removeAt(1);
                     }
               }
               else
               {
                  arguments.append(QCoreApplication::arguments()[0]);
                  // log to console
                  arguments.append("-o"); arguments.append("-,txt");
                  // log to file as TXT
                  arguments.append("-o"); arguments.append(mTestLogFolder % "/" % testName % ".log,txt");
                  // log to file as XML
                  arguments.append("-o"); arguments.append(mTestLogFolder % "/" % testName % ".xml,xunitxml");
               }
               m_overallResult |= QTest::qExec(test, arguments);
            }
         }
      
         QList<QObject *> m_tests;
         int m_overallResult;
         const QString mTestLogFolder = "testLogs";
      };
      
      #endif // TESTRUNNER_H
      

      自己的代码

      #include "testrunner.h"
      #include "test1" 
      ...
      
      #include <QDebug>
      
      int main(int argc, char * argv[]) 
      {
          TestRunner testRunner;
      
          //your QTest compatible class here
          testRunner.addTest(new Test1);
          testRunner.addTest(new Test2);
          ...
      
          bool pass = testRunner.runTests(argc, argv);
          qDebug() << "Overall result: " << (pass ? "PASS" : "FAIL");
      
          return pass?0:1;
      }
      

      【讨论】:

        【解决方案9】:

        如果您使用的是 Qt,我建议您使用 QtTest,因为它具有测试 UI 的功能并且使用简单。

        如果你使用 QtCore,你可以不用 STL。我经常发现 Qt 类比 STL 类更易于使用。

        【讨论】:

          【解决方案10】:

          我一直在玩这个。对我们来说,使用 Google Test 而不是 QtTest 的主要优势是我们在 Visual Studio 中进行所有 UI 开发。如果您使用 Visual Studio 2012 并安装 Google Test Adapter,您可以让 VS 识别测试并将它们包含在其测试资源管理器中。这非常适合开发人员在编写代码时使用,而且由于 Google Test 是可移植的,我们还可以将测试添加到 Linux 构建的末尾。

          我希望将来有人会将对 C++ 的支持添加到 C# 拥有的并发测试工具之一中,例如 NCrunchGilesContinuousTests

          当然,您可能会发现有人为 VS2012 编写了另一个适配器,为测试适配器添加了 QtTest 支持,在这种情况下,这个优势就消失了!如果有人对此感兴趣,有一篇很好的博文Authoring a new Visual studio unit test adapter

          【讨论】:

            【解决方案11】:

            对于带有 QtTest 框架的 Visual Studio 测试适配器工具支持,请使用此 Visual Studio 扩展:https://visualstudiogallery.msdn.microsoft.com/cc1fcd27-4e58-4663-951f-fb02d9ff3653

            【讨论】:

            • 欢迎来到 Stack Overflow!虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。见How do I write a good answer
            猜你喜欢
            • 2010-11-27
            • 2023-03-19
            • 2011-02-20
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-10-02
            相关资源
            最近更新 更多