草庐IT

QT项目搭建完整的单元测试流程

码农飞飞 2023-12-18 原文

在介绍QT的单元测试框架之前,先说一下单元测试。单元测试最重要的就是要将应用拆分成一个个独立的可测试的函数模块。只有将应用拆分成一个个函数模块之后,应用才是可测的。所以开发领域衍生出来了一个概念,Test-driven development(TDD)测试驱动的开发。将应用拆分成一个个独立的可测试的模块之后,我们就可以针对函数模块进行测试编码了。

针对函数模块的各种可能的调用场景编写测试用例,这样每次我们的代码修改的时候,我们都可以通过测试来验证我们的修改是否会对模块的功能产生影响。这样就相当于给我们的软件添加了一层防护网。编写测试用例很早之前都得是靠自己手写的,这样效率很低,后来出现了一些测试框架能帮助开发者自动化完成这一工作,比较出名的测试框架有Google Test和我们今天介绍的Qt Test。

使用Qt的测试框架,首先新建一个测试工程

新建完测试工程之后,我们在工程中添加所有单元测试用例用到的基类,基类中包含了所有测试用例用到的通用方法和属性。

//test-suite.h
#ifndef TESTSUITE_H
#define TESTSUITE_H

#include <QObject>
#include <QString>
#include <QtTest/QtTest>
#include <vector>

namespace test {

class TestSuite : public QObject
{
	Q_OBJECT
public:
	explicit TestSuite(const QString& _testName = "");
	virtual ~TestSuite();

    //测试用例的名称
	QString testName;
    //获取所有实例化的测试用例
	static std::vector<TestSuite*>& testList();
};

}

#endif
//test-suite.cpp
#include "test-suite.h"
#include <QDebug>

namespace test {
TestSuite::TestSuite(const QString& _testName)
    : QObject()
    , testName(_testName)
{
    //只要测试用例创建就会被添加到静态容器中
    qDebug() << "create test:" << testName;
    testList().push_back(this);
    qDebug() << testList().size() << "test added";
}

TestSuite::~TestSuite()
{
    qDebug() << "destory test:" << testName;
}

std::vector<TestSuite*>& TestSuite::testList()
{
	static std::vector<TestSuite*> instance = std::vector<TestSuite*>();
	return instance;
}
}

通过基类的方法和成员变量,我们就可以获取每个测试用例的名称,以及所有实例化的测试用例的列表了。默认的测试工程是没有添加程序的入口函数的,需要手动添加,程序的入口函数如下,在程序入口中我们执行所有的测试用例,并将测试结果输出到xml文件中,为后面的分析和使用做准备。

//main.cpp
#include <QtTest/QtTest>
#include <QDebug>
#include "test-suite.h"
#include "demotest.h"

using namespace test;
int main(int argc, char *argv[])
{
    Q_UNUSED(argc);
    Q_UNUSED(argv);
    
    qDebug() << "found:" << TestSuite::testList().size() << "test suite";

    //记录测试失败的数量
    int failedTestsCount = 0;
    for(TestSuite* i : TestSuite::testList())
    {
        qDebug() << "Executing test " << i->testName;

        //执行每个测试
        QString filename(i->testName + ".xml");

        //执行每个测试并将测试结果输出到xml文件中
        int result = QTest::qExec(i, QStringList() << " " << "-o" << filename << "-xunitxml");
        qDebug() << "Test result " << result;
        if(result != 0)
        {
            failedTestsCount++;
        }
    }

    qDebug() << "Test suite complete:" << failedTestsCount << "failures detected.";

    return failedTestsCount;
}

QTest::qExec支持很多配置参数,这里选几个常用的介绍一下,有需要更加详细的可以去查询官方文档。

-o   filename  format      以指定的格式将测试结果输出到某个文件
-o filename                将测试结果输出到某个文件
-txt                       以纯文本的形式输出测试信息
-xml 		               以XML的形式输出测试信息
-lightxml                  以轻量级xml的形式输出测试信息
-xunitxml                  将测试结果输出成xml文档
-csv                       以CSV的格式输出测试信息
-teamcity                  以TeamCity格式输出结果 

搭建完成测试用例执行的框架之后,我们就可以为每一个单元模块添加测试了,这里以一个简单的模块为例介绍一下测试用例的用法。

这个模块主要有两个方面的功能

1.求两个整数的和

2.求时间的各种表示格式

//demo.h
#ifndef DEMO_H
#define DEMO_H
#include <QObject>
#include <QDateTime>

class Demo : public QObject
{
    Q_OBJECT
public:
    Demo(const QDateTime& time);
    Demo();

public:
    int addTwoNumber(int number1,int number2);
    QString toIso8601String() const;
    QString toPrettyDateString() const;
    QString toPrettyTimeString() const;
    QString toPrettyString() const;
private:
    QDateTime datetime;
signals:
    void inputtwozero();
};

#endif // DEMO_H

//demo.cpp
#include "demo.h"
#include <QLocale>

Demo::Demo(const QDateTime &time):
    QObject(),
    datetime(time)
{}

Demo::Demo()
{
}
int Demo::addTwoNumber(int number1, int number2)
{
    if((number1 == 0) && (number2 == 0))
    {
        emit inputtwozero();
    }
    return number1 + number2;
}

QString Demo::toIso8601String() const
{
    if (datetime.isNull()) {
        return "";
    } else {
        return datetime.toString(Qt::ISODate);
    }
}

QString Demo::toPrettyString() const
{
    if (datetime.isNull()) {
        return "Not set";
    } else {
        QLocale locale = QLocale::English;
        return locale.toString(datetime,"ddd d MMM yyyy @ HH:mm:ss");
    }
}

QString Demo::toPrettyDateString() const
{
    if (datetime.isNull()) {
        return "Not set";
    } else {
        QLocale locale = QLocale::English;
        return locale.toString(datetime,"d MMM yyyy");
    }
}

QString Demo::toPrettyTimeString() const
{
    if (datetime.isNull()) {
        return "Not set";
    } else {
        QLocale locale = QLocale::English;
        return locale.toString(datetime,"hh:mm ap");
    }
}

在测试工程中为单元模块添加测试用例,测试用例添加有几点需要了解。

1.测试用例的初始化函数和清理函数

测试用例中有几个被预留的函数,负责初始化测试用例以及测试用例执行完之后的清理操作。他们分别是:

/// 在第一个测试函数执行之前被调用的初始化函数
void initTestCase();
/// 在最后一个测试函数执行完毕之后调用的清理操作
void cleanupTestCase();

/// 每次执行测试函数之前调用的初始化函数
void init();
/// 每次执行完测试函数之后调用的清理操作
void cleanup();

2.记录某个信号触发的次数

在测试工程中,有时候我们需要验证某个信号是否触发,触发了多少次,这时候我们需要使用QSignalSpy类来记录信号触发的次数。

3.在测试用例的实现文件中添加静态的全局变量,这样在构建工程的时候测试用例就会自动添加到测试用例列表。

测试用例的实现如下:

//demotest.h
#ifndef DEMOTEST_H
#define DEMOTEST_H
#include <QtTest/QtTest>
#include "test-suite.h"

namespace  test {
class DemoTest : public TestSuite
{
    Q_OBJECT
public:
    DemoTest();

private slots:
    /// @brief 第一个测试函数执行之前被调用
    void initTestCase();
    /// @brief 最后一个测试函数执行之后被调用
    void cleanupTestCase();
    /// @brief 每个测试函数执行之前被调用
    void init();
    /// @brief 每个测试函数执行之后被调用
    void cleanup();

    //测试加法运算
    void addTwoNumber_twoPositiveNum_returnInt();
    void addTwoNumber_twoNegative_returnInt();
    void addTwoNumber_nagativeandpostive_returnInt();
    void addTwoNumber_twozero_returnInt();

    //toIso8601String 接口测试(缺省值和外部设置值)
    void toIso8601String_whenDefaultValue_returnsString();
    void toIso8601String_whenValueSet_returnsString();

    //toPrettyDateString 接口测试(缺省值和外部设置值)
    void toPrettyDateString_whenDefaultValue_returnsString();
    void toPrettyDateString_whenValueSet_returnsString();

    //toPrettyTimeString 接口测试(缺省值和外部设置值)
    void toPrettyTimeString_whenDefaultValue_returnsString();
    void toPrettyTimeString_whenValueSet_returnsString();

    //toPrettyTimeString 接口测试(缺省值和外部设置值)
    void toPrettyString_whenDefaultValue_returnsString();
    void toPrettyString_whenValueSet_returnsString();

private:
    //测试数据
    QDateTime testDate{QDate(2022, 6, 4), QTime(16, 40, 32)};
};

}
#endif // DEMOTEST_H

//demotest.cpp
#include "demotest.h"
#include <QDebug>
#include "demo.h"

namespace test {

//实例化静态变量自动添加到列表中
static DemoTest instance;

DemoTest::DemoTest():TestSuite("demoTest")
{
}

void DemoTest::initTestCase()
{
}

void DemoTest::cleanupTestCase()
{
}

void DemoTest::init()
{
}

void DemoTest::cleanup()
{
}

void DemoTest::addTwoNumber_twoPositiveNum_returnInt()
{
    Demo demo;
    QCOMPARE(demo.addTwoNumber(1955,1932), 3887);
}

void DemoTest::addTwoNumber_twoNegative_returnInt()
{
    Demo demo;
    QCOMPARE(demo.addTwoNumber(-1955,-1932), -3887);
}

void DemoTest::addTwoNumber_nagativeandpostive_returnInt()
{
    Demo demo;
    QCOMPARE(demo.addTwoNumber(-1955,1932), -23);
}

void DemoTest::addTwoNumber_twozero_returnInt()
{
    Demo demo;
    QSignalSpy valueChangedSpy(&demo, &Demo::inputtwozero);
    QCOMPARE(demo.addTwoNumber(0,0), 0);
    QCOMPARE(valueChangedSpy.count(), 1);
}

void DemoTest::toIso8601String_whenDefaultValue_returnsString()
{
    Demo demo;
    QCOMPARE(demo.toIso8601String(), QString(""));
}

void DemoTest::toIso8601String_whenValueSet_returnsString()
{
    Demo demo(testDate);
    QString string_value = demo.toIso8601String();
    QCOMPARE(demo.toIso8601String(), QString("2022-06-04T16:40:32"));
}

void DemoTest::toPrettyDateString_whenDefaultValue_returnsString()
{
    Demo demo;
    QCOMPARE(demo.toPrettyDateString(), QString("Not set"));
}

void DemoTest::toPrettyDateString_whenValueSet_returnsString()
{
    Demo demo(testDate);
    QCOMPARE(demo.toPrettyDateString(), QString("4 Jun 2022"));
}

void DemoTest::toPrettyTimeString_whenDefaultValue_returnsString()
{
    Demo demo;
    QCOMPARE(demo.toPrettyTimeString(), QString("Not set"));
}

void DemoTest::toPrettyTimeString_whenValueSet_returnsString()
{
    Demo demo(testDate);
    QString pm = demo.toPrettyTimeString();
    QCOMPARE(demo.toPrettyTimeString(), QString("04:40 pm"));
}

void DemoTest::toPrettyString_whenDefaultValue_returnsString()
{
    Demo demo;
    QCOMPARE(demo.toPrettyString(), QString("Not set"));
}

void DemoTest::toPrettyString_whenValueSet_returnsString()
{
    Demo demo(testDate);
    QCOMPARE(demo.toPrettyString(), QString("周六 4 6月 2022 @ 16:40:32"));
}
}

添加完测试用例之后,执行测试工程,我们就可以在输出目录里面查看测试结果了,通过测试结果,我们就可以知道哪个测试成功,哪个测试失败了。测试文件数据格式如下:

<?xml version="1.0" encoding="UTF-8" ?>
<testsuite errors="0" failures="1" tests="14" name="test::DemoTest">
  <properties>
    <property value="5.9.0" name="QTestVersion"/>
    <property value="5.9.0" name="QtVersion"/>
    <property value="Qt 5.9.0 (i386&#x002D;little_endian&#x002D;ilp32 shared (dynamic) debug build; by MSVC 2015)" name="QtBuild"/>
  </properties>
  <testcase result="pass" name="initTestCase"/>
  <testcase result="pass" name="addTwoNumber_twoPositiveNum_returnInt"/>
  <testcase result="pass" name="addTwoNumber_twoNegative_returnInt"/>
  <testcase result="pass" name="addTwoNumber_nagativeandpostive_returnInt"/>
  <testcase result="pass" name="addTwoNumber_twozero_returnInt"/>
  <testcase result="pass" name="toIso8601String_whenDefaultValue_returnsString"/>
  <testcase result="pass" name="toIso8601String_whenValueSet_returnsString"/>
  <testcase result="pass" name="toPrettyDateString_whenDefaultValue_returnsString"/>
  <testcase result="pass" name="toPrettyDateString_whenValueSet_returnsString"/>
  <testcase result="pass" name="toPrettyTimeString_whenDefaultValue_returnsString"/>
  <testcase result="pass" name="toPrettyTimeString_whenValueSet_returnsString"/>
  <testcase result="pass" name="toPrettyString_whenDefaultValue_returnsString"/>
  <testcase result="fail" name="toPrettyString_whenValueSet_returnsString">
    <failure message="Compared values are not the same
   Actual   (demo.toPrettyString())                : &quot;Sat 4 Jun 2022 @ 16:40:32&quot;
   Expected (QString(&quot;周六 4 6月 2022 @ 16:40:32&quot;)): &quot;\uFFFD\uFFFD\uFFFD\uFFFD 4 6\uFFFD\uFFFD 2022 @ 16:40:32&quot;" result="fail"/>
  </testcase>
  <testcase result="pass" name="cleanupTestCase"/>
  <system-err/>
</testsuite>

有关QT项目搭建完整的单元测试流程的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  3. ruby - 使用 C 扩展开发 ruby​​gem 时,如何使用 Rspec 在本地进行测试? - 2

    我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当

  4. ruby - Ruby 的 Hash 在比较键时使用哪种相等性测试? - 2

    我有一个围绕一些对象的包装类,我想将这些对象用作散列中的键。包装对象和解包装对象应映射到相同的键。一个简单的例子是这样的:classAattr_reader:xdefinitialize(inner)@inner=innerenddefx;@inner.x;enddef==(other)@inner.x==other.xendenda=A.new(o)#oisjustanyobjectthatallowso.xb=A.new(o)h={a=>5}ph[a]#5ph[b]#nil,shouldbe5ph[o]#nil,shouldbe5我试过==、===、eq?并散列所有无济于事。

  5. ruby - RSpec - 使用测试替身作为 block 参数 - 2

    我有一些Ruby代码,如下所示:Something.createdo|x|x.foo=barend我想编写一个测试,它使用double代替block参数x,这样我就可以调用:x_double.should_receive(:foo).with("whatever").这可能吗? 最佳答案 specify'something'dox=doublex.should_receive(:foo=).with("whatever")Something.should_receive(:create).and_yield(x)#callthere

  6. ruby-on-rails - 项目升级后 Pow 不会更改 ruby​​ 版本 - 2

    我在我的Rails项目中使用Pow和powifygem。现在我尝试升级我的ruby​​版本(从1.9.3到2.0.0,我使用RVM)当我切换ruby​​版本、安装所有gem依赖项时,我通过运行railss并访问localhost:3000确保该应用程序正常运行以前,我通过使用pow访问http://my_app.dev来浏览我的应用程序。升级后,由于错误Bundler::RubyVersionMismatch:YourRubyversionis1.9.3,butyourGemfilespecified2.0.0,此url不起作用我尝试过的:重新创建pow应用程序重启pow服务器更新战俘

  7. ruby - Sinatra:运行 rspec 测试时记录噪音 - 2

    Sinatra新手;我正在运行一些rspec测试,但在日志中收到了一堆不需要的噪音。如何消除日志中过多的噪音?我仔细检查了环境是否设置为:test,这意味着记录器级别应设置为WARN而不是DEBUG。spec_helper:require"./app"require"sinatra"require"rspec"require"rack/test"require"database_cleaner"require"factory_girl"set:environment,:testFactoryGirl.definition_file_paths=%w{./factories./test/

  8. ruby-on-rails - 新 Rails 项目 : 'bundle install' can't install rails in gemfile - 2

    我已经像这样安装了一个新的Rails项目:$railsnewsite它执行并到达:bundleinstall但是当它似乎尝试安装依赖项时我得到了这个错误Gem::Ext::BuildError:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcheckingforlibkern/OSAtomic.h...yescreatingMakefilemake"DESTDIR="cleanmake"DESTDIR="

  9. ruby-on-rails - 迷你测试错误 : "NameError: uninitialized constant" - 2

    我遵循MichaelHartl的“RubyonRails教程:学习Web开发”,并创建了检查用户名和电子邮件长度有效性的测试(名称最多50个字符,电子邮件最多255个字符)。test/helpers/application_helper_test.rb的内容是:require'test_helper'classApplicationHelperTest在运行bundleexecraketest时,所有测试都通过了,但我看到以下消息在最后被标记为错误:ERROR["test_full_title_helper",ApplicationHelperTest,1.820016791]test

  10. ruby - 即使失败也继续进行多主机测试 - 2

    我已经构建了一些serverspec代码来在多个主机上运行一组测试。问题是当任何测试失败时,测试会在当前主机停止。即使测试失败,我也希望它继续在所有主机上运行。Rakefile:namespace:specdotask:all=>hosts.map{|h|'spec:'+h.split('.')[0]}hosts.eachdo|host|begindesc"Runserverspecto#{host}"RSpec::Core::RakeTask.new(host)do|t|ENV['TARGET_HOST']=hostt.pattern="spec/cfengine3/*_spec.r

随机推荐