1. 單元測試思考
在不同的公司和不同的項目上,常常會聽到單元測試,但是真正能落實的確實寥寥無幾,無非是在單元測試的開發(fā)時間和回報上模棱兩可。
到底是否需要單元測試嗎?
引用知乎觀點如下:
引用1:
第一個問題應該是,這個公司需要(覆蓋率比較高的)測試么?
對于大部分公司來說,他們的選擇都是不寫。為什么?
首先,他們的產(chǎn)品fail了影響不大。尤其是網(wǎng)頁開發(fā),網(wǎng)頁崩了,顯示對不齊了,移動端看不見內(nèi)容了,這根本不是個事兒。用戶早就習慣了網(wǎng)頁不完全按照他們理想的狀態(tài)工作了。因此對于這些公司來說,有必要把精力放在大規(guī)模的測試上么?完全沒有。試試新功能在自己的瀏覽器下好不好使就可以上線了,上去了有bug大家也不會在意。
其次,他們的產(chǎn)品更新速度快。更新速度是個雙刃劍,比如對于網(wǎng)頁端的開發(fā)來說,更新產(chǎn)品難度極低,發(fā)現(xiàn)了bug可以隨時修復。同時更新的feature比更穩(wěn)定的工作環(huán)境對于他們的產(chǎn)品更重要。所以很多人覺得自己寫代碼就是在寫bug,修復了之前的一些bug,把更多的bug跟著新的feature加進去,未來再修復。反正發(fā)現(xiàn)了可以隨時修復嘛。這里說一句題外話,Tesla的firmware update想法非常牛,節(jié)省了海量召回成本。但是在固件如此容易更新的情況下,是否Tesla會降低對出廠產(chǎn)品穩(wěn)定性的要求,從而讓新的feature更快地面世?不得不說是個隱患。
第三,他們窮。當你的錢只夠開發(fā)或者測試的時候,你選什么?對于大部分小企業(yè)來說這是實實在在的問題。他們恨不得雇一個碼農(nóng)來實現(xiàn)他們偉大的想法,把碼農(nóng)當工地的工人,自己是設計高樓大廈的建筑師。最后的結(jié)果當然是碼農(nóng)只有經(jīng)歷做feature開發(fā),而沒有精力去做大規(guī)模測試。
然后才應該是,這個測試,應該讓誰寫。
我覺得比較合理的方法應該是開發(fā)自己寫一份初稿,然后測試去完善。為什么初稿要開發(fā)去寫?因為開發(fā)者對自己的程序了解程度是最深的,他最清楚自己的程序在哪些corner case容易出錯,也最了解哪些feature應該被測試。如果寫完直接扔給測試,那測試要從頭理解他的代碼,很可能還理解上有偏差,完全是事倍功半。
但是如果讓開發(fā)去完整地寫一個test,第一浪費開發(fā)的時間,第二每個開發(fā)對測試系統(tǒng)的了解也不夠透徹,很可能大家的test長得千奇百怪的。這個部分恰好是測試擅長的。當測試知道了這個單元的大致測試方向,他可以更好地對接測試系統(tǒng),把測試寫得高效,同時有有效的log信息。
最后說說單元測試這個東西,還是要看你以什么為一個「單元」。對于大部分公司來說,對每一個函數(shù)進行「單元測試」的價值都是偏低的。但是如果你有一個比較獨立的功能(當然也可能這個功能就一個函數(shù)),應該對這個功能進行比較詳盡的測試。
當然也要看你對產(chǎn)品的穩(wěn)定性需求。比如Compiler這種東西,那是真的每個標準函數(shù)都要寫單元測試的。。
引用2:
公用組件庫,SDK,所有公共開發(fā)的基礎部分,都需要有嚴格的單元測試保證,而且這些東西變更不會特別頻繁,所以覆蓋率需要100%。
業(yè)務項目,需要自動化接口功能測試,不需要單元測試,一個是成本太高,而是變化太快,并無必要。(PS:沒有測試工程師的團隊除外)另外,業(yè)務feature代碼,都要經(jīng)過師兄的詳細 review!加上測試工程師的全功能回歸,一定程度上代替了單元測試的功能。單測要因人而異!不要為了聽起來高大上就要做單測!最終目的就是保證項目質(zhì)量,而不是炫技,畢竟單測并沒有什么技術含量!
c/c++的單元測試框架
- CUnit C語言環(huán)境下的開發(fā)源碼的白盒測試用例CUnit。CUnit以靜態(tài)庫的形式提供給用戶使用,用戶編寫程序的時候直接鏈接此靜態(tài)庫就可以了。它提供了一個簡單的單元測試框架,并且為常用的數(shù)據(jù)類型提供了豐富的斷言語句支持。
- GTest是Google公司發(fā)布的一個開源C/C++單元測試框架,已被應用于多個開源項目及Google內(nèi)部項目中,知名的例子包括Chrome Web瀏覽器、LLVM編譯器架構(gòu)、Protocol Buffers數(shù)據(jù)交換格式及工具等。
- CppUnit是個基于LGPL 的開源項目,最初版本移植自 JUnit,是一個非常優(yōu)秀的開源測試框架。CppUnit 和 JUnit 一樣主要思想來源于極限編程(XProgramming)。主要功能就是對單元測試進行管理,并可進行自動化測試。
- CxxTest軟件框架提供了一套代碼框架和代碼生成工具。測試人員需要使用C++頭文件定義測試用例。在測試用例中,通過定義測試方法,來打包需要的測試項目。測試方法中可以調(diào)用開發(fā)人員編寫的函數(shù),類,變量,聲明等,來實現(xiàn)對這些函數(shù),類,變量,聲明進行測試和驗證。測試函數(shù)中需要使用CxxTest提供的斷言宏,來測試比較結(jié)果。這些結(jié)果如果非預期,會被作為失敗或者異常,被CxxTest記錄下來。從而實現(xiàn)單元測試自動化。
- Boost.Test功能強大,提供了自動注冊和手動注冊兩種方式,更重要的是來自千錘百煉的Boost庫。
- CppUTest是一個功能全面的測試框架,是為了支持在多種操作系統(tǒng)上開發(fā)嵌入式軟件而特別設計的。CppUTest的宏被設計成不需要了解C++也可以寫測試用例。這使得C程序員更容易用這個測試框架。CppUTest只使用C++語言中主要的那部分子集,這種選擇很好地適應了那些編譯器不能完全支持全部C++語言特性的嵌入式開發(fā)。你會看到用Unity和CppUTest寫出的單元測試幾乎一模一樣。你當然可以選擇任意一個測試框架來進行你的產(chǎn)品開發(fā)。
- CppUnitLite輕量級,簡單易用。
2. Qt的單元測試框架 QTest
Qt Test 是一個用于對基于 Qt 的應用程序和庫進行單元測試的框架。Qt Test 提供了單元測試框架中常見的所有功能以及用于測試圖形用戶界面的擴展。
2.1 Qt Test特性
Qt Test 旨在簡化基于 Qt 的應用程序和庫的單元測試的編寫:
特征 | 細節(jié) |
---|---|
輕量級 | Qt Test 由大約 6000 行代碼和 60 個導出符號組成。 |
自包含 | Qt Test 只需要 Qt Core 模塊中的幾個符號來進行非 gui 測試。 |
快速測試 | Qt Test 不需要特殊的測試運行器;沒有特殊的測試注冊。 |
數(shù)據(jù)驅(qū)動測試 | 可以使用不同的測試數(shù)據(jù)多次執(zhí)行測試。 |
基本 GUI 測試 | Qt Test 提供了鼠標和鍵盤模擬的功能。 |
基準測試 | Qt Test 支持基準測試并提供多個測量后端。 |
IDE 友好 | Qt Test 輸出可由 Qt Creator、Visual Studio 和 KDevelop 解釋的消息。 |
線程安全 | 錯誤報告是線程安全和原子的。 |
類型安全 | 模板的廣泛使用可以防止隱式類型轉(zhuǎn)換引入的錯誤。 |
易于擴展 | 自定義類型可以很容易地添加到測試數(shù)據(jù)和測試輸出中。 |
控制臺輸出效果:
AutoTest輸出效果:
2.3 使用QMake構(gòu)建單元測試
如果您將qmake其用作構(gòu)建工具,只需將以下內(nèi)容添加到您的項目文件中:
QT += testlib
2.4 QTest 命令行參數(shù)
語法
執(zhí)行自動測試的語法采用以下簡單形式:
testname [options] [ testfunctions [:testdata ] ] .. .
替換testname為您的可執(zhí)行文件的名稱。testfunctions可以包含要執(zhí)行的測試函數(shù)的名稱。如果沒有testfunctions通過,則運行所有測試。如果您在 中附加條目的名稱testdata,則測試函數(shù)將僅使用該測試數(shù)據(jù)運行。
例如:
/ myTestDirectory$ testQString toUpper
toUpper運行使用所有可用測試數(shù)據(jù)調(diào)用的測試函數(shù)。
/myTestDirectory$ testQString toUpper toInt:zero
toUpper使用所有可用的測試數(shù)據(jù)運行測試函數(shù),并toInt調(diào)用帶有測試數(shù)據(jù)的測試函數(shù)zero(如果指定的測試數(shù)據(jù)不存在,則關聯(lián)的測試將失敗)。
/ myTestDirectory$ testMyWidget - vs - eventdelay 500
運行testMyWidget功能測試,輸出每個信號發(fā)射并在每個模擬鼠標/鍵盤事件后等待 500 毫秒。
2.3 創(chuàng)建測試
要創(chuàng)建測試,請將QObject子類化并為其添加一個或多個私有槽。每個私有插槽都是您測試中的一個測試函數(shù)。QTest::qExec ()
可用于執(zhí)行測試對象中的所有測試函數(shù)。
class TestQString: public QObject
{
Q_OBJECT
private slots:
void toUpper();
};
此外,您可以定義以下不被視為測試函數(shù)的私有槽。如果存在,它們將由測試框架執(zhí)行,可用于初始化和清理整個測試或當前測試功能。
- initTestCase()將在執(zhí)行第一個測試函數(shù)之前調(diào)用。
- initTestCase_data()將被調(diào)用以創(chuàng)建全局測試數(shù)據(jù)表。
- cleanupTestCase()將在執(zhí)行最后一個測試函數(shù)后調(diào)用。
- init()將在每個測試函數(shù)執(zhí)行之前調(diào)用。
- cleanup()將在每個測試函數(shù)之后調(diào)用。
用于initTestCase
()準備測試。每個測試都應使系統(tǒng)處于可用狀態(tài),以便可以重復運行。清理操作應該在 中處理cleanupTestCase
(),因此即使測試失敗,它們也會運行。
用于init()準備測試功能。每個測試功能都應該使系統(tǒng)處于可用狀態(tài),以便可以重復運行。清理操作應該在 中處理cleanup
(),因此即使測試功能失敗并提前退出,它們也會運行。
或者,您可以使用 RAII(資源獲取是初始化),在析構(gòu)函數(shù)中調(diào)用清理操作,以確保它們在測試函數(shù)返回并且對象移出范圍時發(fā)生。
如果initTestCase
()失敗,則不會執(zhí)行任何測試功能。如果init
()失敗,后面的測試函數(shù)將不被執(zhí)行,測試將進入下一個測試函數(shù)。
例子:
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.");
}
};
最后,如果測試類有一個靜態(tài)公共void initMain()方法,它會在QApplication對象被實例化之前由QTEST_MAIN
宏調(diào)用。例如,這允許設置像Qt::AA_DisableHighDpiScaling
這樣的應用程序?qū)傩?。這是在 5.14 中添加的。
2.4 GUI測試
QTestlib單元測試提供GUI操作函數(shù),可對控件發(fā)送消息后檢測執(zhí)行結(jié)果,比如QTest::keyClick(),QTest::mouseClick()等等。
QTestlib具有測試GUI的一些特性。QTestLib發(fā)送內(nèi)部Qt事件,而不是模擬本地窗口系統(tǒng)事件,因此運行測試程序不會對機器產(chǎn)生任何副作用。
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 <QtWidgets>
#include <QtTest/QtTest>
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 () 宏擴展為運行所有測試函數(shù)的簡單 main() 方法,由于我們的測試類的聲明和實現(xiàn)都在一個 .cpp 文件中,我們還需要包含生成的 moc 文件以使Qt 的自省工作。
在執(zhí)行測試函數(shù)時,我們首先創(chuàng)建一個QLineEdit。然后我們使用QTest::keyClicks ()
函數(shù)在行編輯中模擬寫“hello world” 。
注意:小部件也必須顯示才能正確測試鍵盤快捷鍵。
QTest::keyClicks ()
模擬單擊??小部件上的一系列鍵??蛇x地,可以指定鍵盤修飾符以及每次按鍵單擊后的測試延遲(以毫秒為單位)。類似地,可以使用QTest::keyClick ()
、QTest::keyPress ()
、QTest::keyRelease ()
、QTest::mouseClick ()
、QTest::mouseDClick ()
、QTest::mouseMove ()
、QTest::mousePress ()
和QTest::mouseRelease ()
函數(shù)模擬相關的GUI事件。
2.5 數(shù)據(jù)驅(qū)動測試
1. 數(shù)據(jù)驅(qū)動簡介
目前為止,采用硬編碼的方式將測試數(shù)據(jù)寫到測試函數(shù)中。如果增加更多的測試數(shù)據(jù),那么測試函數(shù)會變成:
QCOMPARE(QString("hello").toUpper(), QString("HELLO"));
QCOMPARE(QString("Hello").toUpper(), QString("HELLO"));
QCOMPARE(QString("HellO").toUpper(), QString("HELLO"));
QCOMPARE(QString("HELLO").toUpper(), QString("HELLO"));
為了不使測試函數(shù)被重復的代碼弄得凌亂不堪, QTestLib支持向測試函數(shù)增加測試數(shù)據(jù),僅需要向測試類增加另一個私有槽:
class TestQString: public QObject
{
Q_OBJECT
private slots:
void toUpper_data();
void toUpper();
};
2、編寫測試數(shù)據(jù)函數(shù)
為測試函數(shù)提供數(shù)據(jù)的函數(shù)必須與測試函數(shù)同名,并加上_data后綴。為測試函數(shù)提供數(shù)據(jù)的函數(shù)類似這樣:
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()函數(shù)定義測試數(shù)據(jù)表的兩列元素:測試字符串和在該測試字符串上調(diào)用QString::toUpper()函數(shù)期望得到的結(jié)果。
然后使用 QTest::newRow()函數(shù)向測試數(shù)據(jù)表中增加一些數(shù)據(jù)。每組數(shù)據(jù)都會成為測試數(shù)據(jù)表中的一個單獨的行。
QTest::newRow()函數(shù)接收一個參數(shù):將要關聯(lián)到該行測試數(shù)據(jù)的名字。如果測試函數(shù)執(zhí)行失敗,名字會被測試日志使用,以引用導致測試失敗的數(shù)據(jù)。然后將測試數(shù)據(jù)加入到新行:首先是一個任意的字符串,然后是在該行字符串上調(diào)用 QString::toUpper()函數(shù)期望得到的結(jié)果字符串。
可以將測試數(shù)據(jù)看作是一張二維表格。在這個例子里,它包含兩列三行,列名為string 和result。另外,每行都會對應一個序號和名稱:
index name string result
0 all lower “hello” HELLO
1 mixed “Hello” HELLO
2 all upper “HELLO” HELLO
3、編寫測試函數(shù)
測試函數(shù)需要被重寫:
void TestQString::toUpper()
{
QFETCH(QString, string);
QFETCH(QString, result);
QCOMPARE(string.toUpper(), result);
}
TestQString::toUpper()函數(shù)會執(zhí)行兩次,對toUpper_data()函數(shù)向測試數(shù)據(jù)表中加入的每一行都會調(diào)用一次。
首先,調(diào)用QFETCH()宏從測試數(shù)據(jù)表中取出兩個元素。QFETCH()接收兩個參數(shù): 元素的數(shù)據(jù)類型和元素的名稱。然后用QCOMPARE()宏執(zhí)行測試操作。
使用這種方法可以不修改測試函數(shù)就向該函數(shù)加入新的數(shù)據(jù)。
像以前一樣,為使測試程序能夠單獨執(zhí)行,需要加入下列代碼:
QTEST_MAIN(TestGui)
結(jié)合工程實例 (多項測試集成測試)
來自 https://github.com/ddqd/QAutoTest
本實例提供了一個已一個最簡單的demo來模擬單元測試的實際應用。
主工程 pro
TEMPLATE = subdirs
SUBDIRS += \
tests
SUBDIRS += \
Calc
文件夾目錄結(jié)構(gòu)
c++ 被測試模塊
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 <QObject>
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 <QtTest/qtest.h>
#include <QList>
#include <QString>
#include <QSharedPointer>
#include <QApplication>
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 <QObject>
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 <QTest>
#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文章來源:http://www.zghlxwxcb.cn/news/detail-401976.html
#include "AutoTest.h"
#include <QDebug>
#include <QCoreApplication>
// 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
其他細節(jié)可參考 官方文檔
https://doc.qt.io/qt-5/qttest-index.html文章來源地址http://www.zghlxwxcb.cn/news/detail-401976.html
到了這里,關于QTest 單元測試框架及單元測試思考的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!