在不同的公司和不同的项目上,常常会听到单元测试,但是真正能落实的确实寥寥无几,无非是在单元测试的开发时间和回报上模棱两可。
到底是否需要单元测试吗?
引用知乎观点如下:
第一个问题应该是,这个公司需要(覆盖率比较高的)测试么?
对于大部分公司来说,他们的选择都是不写。为什么?
首先,他们的产品fail了影响不大。尤其是网页开发,网页崩了,显示对不齐了,移动端看不见内容了,这根本不是个事儿。用户早就习惯了网页不完全按照他们理想的状态工作了。因此对于这些公司来说,有必要把精力放在大规模的测试上么?完全没有。试试新功能在自己的浏览器下好不好使就可以上线了,上去了有bug大家也不会在意。
其次,他们的产品更新速度快。更新速度是个双刃剑,比如对于网页端的开发来说,更新产品难度极低,发现了bug可以随时修复。同时更新的feature比更稳定的工作环境对于他们的产品更重要。所以很多人觉得自己写代码就是在写bug,修复了之前的一些bug,把更多的bug跟着新的feature加进去,未来再修复。反正发现了可以随时修复嘛。这里说一句题外话,Tesla的firmware update想法非常牛,节省了海量召回成本。但是在固件如此容易更新的情况下,是否Tesla会降低对出厂产品稳定性的要求,从而让新的feature更快地面世?不得不说是个隐患。
第三,他们穷。当你的钱只够开发或者测试的时候,你选什么?对于大部分小企业来说这是实实在在的问题。他们恨不得雇一个码农来实现他们伟大的想法,把码农当工地的工人,自己是设计高楼大厦的建筑师。最后的结果当然是码农只有经历做feature开发,而没有精力去做大规模测试。
然后才应该是,这个测试,应该让谁写。
我觉得比较合理的方法应该是开发自己写一份初稿,然后测试去完善。为什么初稿要开发去写?因为开发者对自己的程序了解程度是最深的,他最清楚自己的程序在哪些corner case容易出错,也最了解哪些feature应该被测试。如果写完直接扔给测试,那测试要从头理解他的代码,很可能还理解上有偏差,完全是事倍功半。
但是如果让开发去完整地写一个test,第一浪费开发的时间,第二每个开发对测试系统的了解也不够透彻,很可能大家的test长得千奇百怪的。这个部分恰好是测试擅长的。当测试知道了这个单元的大致测试方向,他可以更好地对接测试系统,把测试写得高效,同时有有效的log信息。
最后说说单元测试这个东西,还是要看你以什么为一个「单元」。对于大部分公司来说,对每一个函数进行「单元测试」的价值都是偏低的。但是如果你有一个比较独立的功能(当然也可能这个功能就一个函数),应该对这个功能进行比较详尽的测试。
当然也要看你对产品的稳定性需求。比如Compiler这种东西,那是真的每个标准函数都要写单元测试的。。
公用组件库,SDK,所有公共开发的基础部分,都需要有严格的单元测试保证,而且这些东西变更不会特别频繁,所以覆盖率需要100%。
业务项目,需要自动化接口功能测试,不需要单元测试,一个是成本太高,而是变化太快,并无必要。(PS:没有测试工程师的团队除外)另外,业务feature代码,都要经过师兄的详细 review!加上测试工程师的全功能回归,一定程度上代替了单元测试的功能。单测要因人而异!不要为了听起来高大上就要做单测!最终目的就是保证项目质量,而不是炫技,毕竟单测并没有什么技术含量!
Qt Test 是一个用于对基于 Qt 的应用程序和库进行单元测试的框架。Qt Test 提供了单元测试框架中常见的所有功能以及用于测试图形用户界面的扩展。
Qt Test 旨在简化基于 Qt 的应用程序和库的单元测试的编写:
特征 | 细节 |
---|---|
轻量级 | Qt Test 由大约 6000 行代码和 60 个导出符号组成。 |
自包含 | Qt Test 只需要 Qt Core 模块中的几个符号来进行非 gui 测试。 |
快速测试 | Qt Test 不需要特殊的测试运行器;没有特殊的测试注册。 |
数据驱动测试 | 可以使用不同的测试数据多次执行测试。 |
基本 GUI 测试 | Qt Test 提供了鼠标和键盘模拟的功能。 |
基准测试 | Qt Test 支持基准测试并提供多个测量后端。 |
IDE 友好 | Qt Test 输出可由 Qt Creator、Visual Studio 和 KDevelop 解释的消息。 |
线程安全 | 错误报告是线程安全和原子的。 |
类型安全 | 模板的广泛使用可以防止隐式类型转换引入的错误。 |
易于扩展 | 自定义类型可以很容易地添加到测试数据和测试输出中。 |
如果您将qmake其用作构建工具,只需将以下内容添加到您的项目文件中:
QT += testlib
执行自动测试的语法采用以下简单形式:
testname [options] [ testfunctions [:testdata ] ] .. .
替换testname为您的可执行文件的名称。testfunctions可以包含要执行的测试函数的名称。如果没有testfunctions通过,则运行所有测试。如果您在 中附加条目的名称testdata,则测试函数将仅使用该测试数据运行。
例如:
/ myTestDirectory$ testQString toUpper
toUpper运行使用所有可用测试数据调用的测试函数。
/myTestDirectory$ testQString toUpper toInt:zero
toUpper使用所有可用的测试数据运行测试函数,并toInt调用带有测试数据的测试函数zero(如果指定的测试数据不存在,则关联的测试将失败)。
/ myTestDirectory$ testMyWidget - vs - eventdelay 500
运行testMyWidget功能测试,输出每个信号发射并在每个模拟鼠标/键盘事件后等待 500 毫秒。
要创建测试,请将QObject子类化并为其添加一个或多个私有槽。每个私有插槽都是您测试中的一个测试函数。QTest::qExec ()
可用于执行测试对象中的所有测试函数。
class TestQString: public QObject
{
Q_OBJECT
private slots:
void toUpper();
};
此外,您可以定义以下不被视为测试函数的私有槽。如果存在,它们将由测试框架执行,可用于初始化和清理整个测试或当前测试功能。
用于initTestCase
()准备测试。每个测试都应使系统处于可用状态,以便可以重复运行。清理操作应该在 中处理cleanupTestCase
(),因此即使测试失败,它们也会运行。
用于init()准备测试功能。每个测试功能都应该使系统处于可用状态,以便可以重复运行。清理操作应该在 中处理cleanup
(),因此即使测试功能失败并提前退出,它们也会运行。
或者,您可以使用 RAII(资源获取是初始化),在析构函数中调用清理操作,以确保它们在测试函数返回并且对象移出范围时发生。
如果initTestCase
()失败,则不会执行任何测试功能。如果init
()失败,后面的测试函数将不被执行,测试将进入下一个测试函数。
例子:
class MyFirstTest: public QObject
{
Q_OBJECT
private:
bool myCondition()
{
return true;
}
private slots:
void initTestCase()
{
qDebug("Called before everything else.");
}
void myFirstTest()
{
QVERIFY(true); // check that a condition is satisfied
QCOMPARE(1, 1); // compare two values
}
void mySecondTest()
{
QVERIFY(myCondition());
QVERIFY(1 != 2);
}
void cleanupTestCase()
{
qDebug("Called after myFirstTest and mySecondTest.");
}
};
最后,如果测试类有一个静态公共void initMain()方法,它会在QApplication对象被实例化之前由QTEST_MAIN
宏调用。例如,这允许设置像Qt::AA_DisableHighDpiScaling
这样的应用程序属性。这是在 5.14 中添加的。
QTestlib单元测试提供GUI操作函数,可对控件发送消息后检测执行结果,比如QTest::keyClick(),QTest::mouseClick()等等。
QTestlib具有测试GUI的一些特性。QTestLib发送内部Qt事件,而不是模拟本地窗口系统事件,因此运行测试程序不会对机器产生任何副作用。
QTest::keyClick(myWidget, 'a');
QTest::keyClicks(myWidget, "hello world");
QTest::keyClick(myWindow, Qt::Key_Escape);
QTest::keyClick(myWindow, Qt::Key_Escape, Qt::ShiftModifier, 200);
举例:
#include
#include
class TestGui: public QObject
{
Q_OBJECT
private slots:
void testGui();
};
void TestGui::testGui()
{
QLineEdit lineEdit;
QTest::keyClicks(&lineEdit, "hello world");
QCOMPARE(lineEdit.text(), QString("hello world"));
}
QTEST_MAIN(TestGui)
#include "testgui.moc"
QTEST_MAIN () 宏扩展为运行所有测试函数的简单 main() 方法,由于我们的测试类的声明和实现都在一个 .cpp 文件中,我们还需要包含生成的 moc 文件以使Qt 的自省工作。
在执行测试函数时,我们首先创建一个QLineEdit。然后我们使用QTest::keyClicks ()
函数在行编辑中模拟写“hello world” 。
注意:小部件也必须显示才能正确测试键盘快捷键。
QTest::keyClicks ()
模拟单击小部件上的一系列键。可选地,可以指定键盘修饰符以及每次按键单击后的测试延迟(以毫秒为单位)。类似地,可以使用QTest::keyClick ()
、QTest::keyPress ()
、QTest::keyRelease ()
、QTest::mouseClick ()
、QTest::mouseDClick ()
、QTest::mouseMove ()
、QTest::mousePress ()
和QTest::mouseRelease ()
函数模拟相关的GUI事件。
目前为止,采用硬编码的方式将测试数据写到测试函数中。如果增加更多的测试数据,那么测试函数会变成:
QCOMPARE(QString("hello").toUpper(), QString("HELLO"));
QCOMPARE(QString("Hello").toUpper(), QString("HELLO"));
QCOMPARE(QString("HellO").toUpper(), QString("HELLO"));
QCOMPARE(QString("HELLO").toUpper(), QString("HELLO"));
为了不使测试函数被重复的代码弄得凌乱不堪, QTestLib支持向测试函数增加测试数据,仅需要向测试类增加另一个私有槽:
class TestQString: public QObject
{
Q_OBJECT
private slots:
void toUpper_data();
void toUpper();
};
为测试函数提供数据的函数必须与测试函数同名,并加上_data后缀。为测试函数提供数据的函数类似这样:
void TestQString::toUpper_data()
{
QTest::addColumn<QString>("string");
QTest::addColumn<QString>("result");
QTest::newRow("all lower") << "hello" << "HELLO";
QTest::newRow("mixed") << "Hello" << "HELLO";
QTest::newRow("all upper") << "HELLO" << "HELLO";
}
首先,使用QTest::addColumn()函数定义测试数据表的两列元素:测试字符串和在该测试字符串上调用QString::toUpper()函数期望得到的结果。
然后使用 QTest::newRow()函数向测试数据表中增加一些数据。每组数据都会成为测试数据表中的一个单独的行。
QTest::newRow()函数接收一个参数:将要关联到该行测试数据的名字。如果测试函数执行失败,名字会被测试日志使用,以引用导致测试失败的数据。然后将测试数据加入到新行:首先是一个任意的字符串,然后是在该行字符串上调用 QString::toUpper()函数期望得到的结果字符串。
可以将测试数据看作是一张二维表格。在这个例子里,它包含两列三行,列名为string 和result。另外,每行都会对应一个序号和名称:
index name string result
0 all lower “hello” HELLO
1 mixed “Hello” HELLO
2 all upper “HELLO” HELLO
测试函数需要被重写:
void TestQString::toUpper()
{
QFETCH(QString, string);
QFETCH(QString, result);
QCOMPARE(string.toUpper(), result);
}
TestQString::toUpper()函数会执行两次,对toUpper_data()函数向测试数据表中加入的每一行都会调用一次。
首先,调用QFETCH()宏从测试数据表中取出两个元素。QFETCH()接收两个参数: 元素的数据类型和元素的名称。然后用QCOMPARE()宏执行测试操作。
使用这种方法可以不修改测试函数就向该函数加入新的数据。
像以前一样,为使测试程序能够单独执行,需要加入下列代码:
QTEST_MAIN(TestGui)
来自 https://github.com/ddqd/QAutoTest
本实例提供了一个已一个最简单的demo来模拟单元测试的实际应用。
主工程 pro
TEMPLATE = subdirs
SUBDIRS += \
tests
SUBDIRS += \
Calc
calc pro文件:
QT += core
QT -= gui
TARGET = Calc
CONFIG += console
CONFIG -= app_bundle
TEMPLATE = app
SOURCES += main.cpp
HEADERS += \
compare.h
SOURCES += \
compare.cpp
#ifndef COMPARE_H
#define COMPARE_H
#include
class Compare : public QObject
{
Q_OBJECT
public:
explicit Compare(QObject *parent = 0);
public slots:
int min(int a, int b)
{
return a<b ? a : b;
}
};
#endif // COMPARE_H
test.pro文件
QT += testlib widgets
HEADERS += \
comparetest.h \
AutoTest.h \
../Calc/compare.h
SOURCES += \
main.cpp \
comparetest.cpp \
../Calc/compare.cpp
AutoTest.h
#ifndef AUTOTEST_H
#define AUTOTEST_H
#include
#include
#include
#include
#include
namespace AutoTest {
typedef QList<QObject*> TestList;
inline TestList& testList(){
static TestList list;
return list;
}
inline bool findObject(QObject* object) {
TestList& list = testList();
if (list.contains(object)) {
return true;
}
foreach (QObject* test, list) {
if (test->objectName() == object->objectName()) {
return true;
}
}
return false;
}
inline void addTest(QObject* object) {
TestList& list = testList();
if (!findObject(object)) {
list.append(object);
}
}
inline int run(int argc, char *argv[]) {
int ret = 0;
foreach (QObject* test, testList()) {
ret += QTest::qExec(test, argc, argv);
}
return ret;
}
}
template <class T>
class Test {
public:
QSharedPointer<T> child;
Test(const QString& name) : child(new T) {
child->setObjectName(name);
AutoTest::addTest(child.data());
}
};
#define DECLARE_TEST(className) static Test<className> t(#className);
#define TEST_MAIN \
int main(int argc, char *argv[]) { \
QApplication app(argc, argv); \
app.setAttribute(Qt::AA_Use96Dpi, true); \
QTEST_DISABLE_KEYPAD_NAVIGATION \
return AutoTest::run(argc, argv); \
}
#endif // AUTOTEST_H
#ifndef COMPARETEST_H
#define COMPARETEST_H
#include "AutoTest.h"
#include
class CompareTest : public QObject
{
Q_OBJECT
public:
explicit CompareTest(QObject *parent = 0);
signals:
public slots:
private slots:
void min();
};
DECLARE_TEST(CompareTest)
#endif // COMPARETEST_H
#include "comparetest.h"
#include
#include "../Calc/compare.h"
CompareTest::CompareTest(QObject *parent) :
QObject(parent) {
}
void CompareTest::min() {
Compare a;
QCOMPARE(a.min(1, 0), 0);
QCOMPARE(a.min(-1, 1), -1);
QCOMPARE(a.min(4, 8), 4);
QCOMPARE(a.min(0, 0), 0);
QCOMPARE(a.min(1, 1), 1);
QCOMPARE(a.min(-10,-5), -10);
}
main.cpp
#include "AutoTest.h"
#include
#include
// This is all you need to run all the tests
#if 1
TEST_MAIN
#else
Or supply your own main function
int main(int argc, char *argv[]) {
int failures = AutoTest::run(argc, argv);
if (failures == 0) {
qDebug() << "ALL TESTS PASSED";
} else {
qDebug() << failures << " TESTS FAILED!";
}
return failures;
}
#endif
https://doc.qt.io/qt-5/qttest-index.html