1. 程式人生 > >Qt高級——QTestLib單元測試框架

Qt高級——QTestLib單元測試框架

QTestLib 單元測試

Qt高級——QTestLib單元測試框架

一、QTestLib簡介

1、QTestLib簡介

QTestLib是Qt提供的一種針對基於Qt編寫的程序或庫的單元測試框架。QTestLib提供了單元測試框架的基本功能,並提供了針對GUI測試的擴展功能。

2、QTestLib特性

QTestLib是為了簡化QT程序或庫的單元測試工作而設計的。
QTestLib特性如下:
A、輕量級:QTestlib只包含6000行代碼和60個導出符號
B、自包含:對於非GUI測試,QTestlib只需要Qt核心庫的幾個符號。
C、快速測試:QTestlib不需要特殊的測試執行程序,不需要為測試而進行特殊的註冊。
D、數據驅動測試:一個測試程序可以在不同的測試數據集上執行多次。

E、基本的GUI測試:QTestlib提供了模擬鼠標和鍵盤事件的功能。
F、基準測試:QTestLIB支持基準測試並提供多種測量後端。
G、IDE友好:QTestlib的輸出信息可以被Visual?Studio和KDevelop解析。
H、線程安全:錯誤報告是線程安全的、原子性的。
J、類型安全:對模板進行了擴展使用,防止由隱式類型轉換引起的錯誤。
K、易擴展:用戶自定義類型可以容易地加入到測試數據和測試輸出中。

3、QTestLib API

所有公有的方法都在QTest命名空間中。另外,QSignalSpy類為Qt的信號和槽提供了簡單的內省機制。

4、AutoTest插件

默認測試結果以純文本形式顯示在控制臺(應用程序輸出標簽),不夠直觀,可使用AutoTest插件實現可視化效果。

通過Help->About Plugins->Utilities,選中AutoTest,重啟Qt Creator,然後在下方會多出TestResults的標簽,可直接在此標簽點擊上方的運行按鈕運行所有測試,同時在“Tools-Tests-Run All Tests”也可運行所有測試。
此插件可以在運行單元測試後以紅、綠色表示明確標記處運行結果,並且以Case為單位顯示,可以展開看到具體每一個測試用例的結果。
技術分享圖片

二、QTestLib單元測試原理

1、QTestLib單元測試原理簡介

原理:輸入測試數據表和結果數據表,與實際值比較。

2、測試類

測試類需要從QObject類繼承,類中需要加入一個或者多個私有槽。每一個私有槽都是一個測試函數,但有4種私有槽不能作為測試函數,它們由測試框架執行,可為整個測試程序或當前測試函數進行初始化和清除操作。

initTestCase():在第一個測試函數執行前調用。
cleanupTestCase():在最後一個測試函數執行後調用。
init():在每一個測試函數執行前調用。
cleanup():在每一個測試函數執行後調用。
如果initTestCase()函數執行失敗,任何測試函數都不會執行。如果init()函數執行失敗,緊隨其後的測試函數不會被執行,測試會繼續處理下一個測試函數。
QTest::qExec(QObject* testClassObject)函數用於執行測試對象中所有的測試函數。

3、測試函數

對於一個要測試的目標函數,需要使用兩個函數進行測試:testFunctionName()和testFunctionName_data()。
testFunctionName_data:數據提供,在函數體中寫入測試數據。
testFunctionName:測試的實體,讀取testFunctionName_data函數中的數據表,並逐行進行測試。如果測試結果與數據表中的結果不同,則認為測試失敗。

4、測試數據構建

數據由QTest::addColumn<T>(name)QTest::newRow(name)<<input<<result來構建一個數據表,其中的列可以被獲取,然後將表中對應的數據按行測試,並與表中的結果列進行對比。

5、GUI測試

對於GUI交互操作的測試,則將數據設置為事件列表,供模擬測試。QTestlib提供了模擬鼠標和鍵盤事件的功能。

6、通信

QTest提供一系列宏來進行數據的通信。

QBENCHMARK
QBENCHMARK_ONCE
QCOMPARE(actual, expected)
QEXPECT_FAIL(dataIndex, comment, mode)
QFAIL(message)
QFETCH(type, name)
QFINDTESTDATA(filename)
QSKIP(description)
QTEST(actual, testElement)
QTEST_APPLESS_MAIN(TestClass)
QTEST_GUILESS_MAIN(TestClass)
QTEST_MAIN(TestClass)
QTRY_COMPARE(actual, expected)
QTRY_COMPARE_WITH_TIMEOUT(actual, expected, timeout)
QTRY_VERIFY2(condition, message)
QTRY_VERIFY(condition)
QTRY_VERIFY2_WITH_TIMEOUT(condition, message, timeout)
QTRY_VERIFY_WITH_TIMEOUT(condition, timeout)
QVERIFY2(condition, message)
QVERIFY(condition)
QVERIFY_EXCEPTION_THROWN(expression, exceptiontype)
QWARN(message)

7、程序啟動入口

QTest提供了QTEST_MAIN()作為測試的啟動宏,構建一個main函數,在main函數內調用QTest::qExec(QObject testClassObject),也可以直接調用QTest::qExec(QObject testClassObject)來啟動測試。

三、簡單測試程序

1、編寫測試程序

假設要測試QString類的行為。首先,需要一個用於包含測試函數的必須從QObject繼承的類:

#include <QtTest/QtTest>

 class TestQString: public QObject
 {
     Q_OBJECT
 private slots:
     void toUpper();
 };

註意:需要包含QTest頭文件,並且測試函數必須聲明為私有槽,便於測試框架找到並執行它們。

void TestQString::toUpper()
 {
     QString str = "Hello";
     QVERIFY(str.toUpper() == "HELLO");
 }

QVERIFY()宏將計算傳入的表達式的值。如果為真,則測試函數繼續進行;否則會向測試日誌中增加一條描述錯誤的信息,並且該測試函數會停止執行。
但是如果需要向測試日誌中增加更詳細的輸出信息,應該使用QCOMPARE()宏:

void TestQString::toUpper()
 {
     QString str = "Hello";
     QCOMPARE(str.toUpper(), QString("HELLO"));
 }

2、執行測試程序

寫完測試程序後就需要執行測試程序。假設將測試程序命名為testqstring.cpp並保存在一個空目錄中,可以使用qmake生成一個工程文件和一個Makefile文件。

myTestDirectory$ qmake -project "QT += qtestlib"
    myTestDirectory$ qmake
    myTestDirectory$ make

技術分享圖片

3、QTestlib命令行參數

執行自動測試的語法形式:
testname?[options]?[testfunctions[:testdata]]...
testname:測試項目的可執行文件
testfunctions:包含要執行的測試函數名,如果不指定testfunctions,所有的測試函數都會執行。如果測試函數名之後加上了測試數據行的名字,則測試函數執行時只會使用該行測試數據。
列如:
/myTestDirectory#?StringTest toUpper
使用所有的測試數據執行toUpper測試函數。
/myTestDirectory$?StringTest??toUpper??toInt:zero
使用所有的測試數據執行toUpper測試函數,使用行名為zero的測試數據執行toInt測試函數(如果對應的測試數據不存在,相關的測試執行時就會失敗)。
/myTestDirectory$?WidgetTest??-vs??-eventdelay??500
執行WidgetTest測試程序,輸出每一個信號發射信息,在每次模擬鼠標/鍵盤事件之後等待500毫秒。
選項
下列命令行參數可以被接受:
-help
輸出命令行參數的幫助信息。
-functions?
輸出測試中的所有測試函數。
-o?filename?
將輸出信息寫入到執行文件中,而不是打印到標準輸出上。
-silent?
沈默地輸出,只顯示警告、錯誤和最少的狀態信息。
-v1?
詳細輸出;輸出每次進入或離開測試函數的信息。
-v2?
詳細輸出;也輸出每個QCOMPARE()和QVERIFY()信息。
-vs?
輸出發出的所有信號。
-xml?
將輸出格式化成XML格式,而不是普通文本
-lightxml?
輸出成XML標簽流。
-eventdelay?ms?
如果鍵盤或鼠標模擬(QTest::keyClick(),QTest::mouseClick()等)不指定延遲時間,則使用該參數(以毫秒為單位)作為延遲時間。
-keydelay?ms
與-eventdelay的作用一樣,但只影響鍵盤模擬的延遲時間,不影響鼠標模擬的延遲時間。
-mousedelay?ms?
與-eventdelay的作用一樣,但只影響鼠標模擬的延遲時間,不影響鍵盤模擬的延遲時間。
-keyevent-verbose?
詳細輸出鍵盤模擬信息。
-maxwarnings?numberBR?
設置警告信息的最大數量,0表示不限制,默認值為2000。

四、數據驅動測試程序

1、數據驅動測試簡介

目前為止,采用硬編碼的方式將測試數據寫到測試函數中。如果增加更多的測試數據,那麽測試函數會變成:

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

2、編寫測試數據函數

為測試函數提供數據的函數必須與測試函數同名,並加上_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

3、編寫測試函數

測試函數需要被重寫:

void TestQString::toUpper()
 {
     QFETCH(QString, string);
     QFETCH(QString, result);

     QCOMPARE(string.toUpper(), result);
 }

TestQString::toUpper()函數會執行兩次,對toUpper_data()函數向測試數據表中加入的每一行都會調用一次。
首先,調用QFETCH()宏從測試數據表中取出兩個元素。QFETCH()接收兩個參數: 元素的數據類型和元素的名稱。然後用QCOMPARE()宏執行測試操作。
使用這種方法可以不修改測試函數就向該函數加入新的數據。
像以前一樣,為使測試程序能夠單獨執行,需要加入下列代碼:
QTEST_MAIN(TestGui)
QTEST_MAIN()宏將擴展成一個簡單的main()函數,該main()函數會執行所有的測試函數。

五、GUI測試

QTestlib單元測試提供GUI操作函數,可對控件發送消息後檢測執行結果,比如QTest::keyClick(),QTest::mouseClick()等等

1、模擬GUI事件

QTestlib具有測試GUI的一些特性。QTestLib發送內部Qt事件,而不是模擬本地窗口系統事件,因此運行測試程序不會對機器產生任何副作用。

#include <QtGui>
#include <QtTest/QtTest>

 class TestGui: public QObject
 {
     Q_OBJECT
 private slots:
     void testGui();
 };

唯一的區別是除了要加入QTest命名空間之外,需要包含QtGui類的定義。

void TestGui::testGui()
 {
     QLineEdit lineEdit;
     QTest::keyClicks(&lineEdit, "hello world");
     QCOMPARE(lineEdit.text(), QString("hello world"));
 }

在測試函數實現中,創建一個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()宏來檢驗行編輯框的文本是否與預期的一致。
像前面一樣,為使測試程序能夠單獨執行,需要加入下列代碼:
QTEST_MAIN(TestGui)
QTEST_MAIN()宏將擴展成一個簡單的main()函數,該main()函數會執行所有的測試函數。

2、重復GUI事件

在本節中,將展示如何模擬GUI事件,以及如何存儲一系列GUI事件以及如何在組件上重復這些GUI事件。
將一系列GUI事件保存起來並重復觸發的方法與數據驅動測試程序的方法很類似。所要做的只是向測試類增加一個提供測試數據的函數:

class TestGui: public QObject
 {
     Q_OBJECT
 private slots:
     void testGui_data();
     void testGui();
 }; 

像前面一樣,為測試函數提供數據的函數必須與該測試函數同名,並加上_data後綴。

void TestGui::testGui_data()
 {
     QTest::addColumn<QTestEventList>("events");
     QTest::addColumn<QString>("expected");

     QTestEventList list1;
     list1.addKeyClick(‘a‘);
     QTest::newRow("char") << list1 << "a";

     QTestEventList list2;
     list2.addKeyClick(‘a‘);
     list2.addKeyClick(Qt::Key_Backspace);
     QTest::newRow("there and back again") << list2 << "";
 }

首先,用QTest::addColumn()函數定義測試數據表的元素:一個GUI事件列表,以及在控件上應用該事件列表預期得到的結果。註意第一個元素的類型是QTestEventList。
QTestEventList可以保存將來要使用的GUI事件,並可以在任意控件上重復觸發這些事件。
在目前的提供測試數據的函數中,創建了兩個QTestEventLists。第一個鏈表包括了一個敲擊“a“鍵事件,調用QTestEventList::addKeyClick()函數向鏈表中加入該事件。然後用QTest::newRow()函數給該行數據指定一個名字,並把事件隊列和期望結果輸入到測試數據表中。
第二個鏈表包括兩次鍵盤敲擊:一個“a“,然後是一個“backspace“。同樣用 QTestEventList::addKeyClick()函數將事件加入隊列,用QTest::newRow()將事件隊列和期望的結果加入測試數據表中,並為該行指定一個名字。

void TestGui::testGui()
 {
     QFETCH(QTestEventList, events);
     QFETCH(QString, expected);

     QLineEdit lineEdit;

     events.simulate(&lineEdit);

     QCOMPARE(lineEdit.text(), expected);
 }

TestGui::testGui()函數會執行兩次,對在TestGui::testGui_data()函數中創建的每一行測試數據都執行一次。
首先,用QFETCH()宏從測試數據集中取出兩個元素。QFETCH()宏接收兩個參數:元素的數據類型和元素的名字。然後創建了一個QLineEdit,調用 QTestEventList::simulate()函數在控件上觸發事件隊列。
最後,用QCOMPARE()宏檢測行編輯框的內容是否與期望的一致。
像以前一樣,為使測試程序能夠單獨執行,需要加入下列代碼:
QTEST_MAIN(TestGui)
QTEST_MAIN()宏將擴展成一個簡單的main()函數,該main()函數會執行所有的測試函數。

六、Benchmark測試

為了編寫一個基準測試程序,需要使用QBENCHMARK宏來擴展測試函數。一個基準測試函數通常由初始化代碼和一個QBENCHMARK宏組成,QBENCHMARK宏包含了需要被測試的代碼。

1、編寫一個基準測試函數

測試函數會對QString::localeAwareCompare()函數進行基準測試。

void TestBenchmark::simple()
 {
     QString str1 = QLatin1String("This is a test string");
     QString str2 = QLatin1String("This is a test string");

     QCOMPARE(str1.localeAwareCompare(str2), 0);

     QBENCHMARK {
         str1.localeAwareCompare(str2);
     }
 }

初始化部分將在函數的開頭被完成,但時鐘並不在這點運行。內嵌在QBENCHMARK宏中的代碼將被估量,並且為了得出精確的測量將會被重復數次。

2、多數據輸入的基準測試

當創建對多個數據輸入進行比較的基準測試時,數據函數是有用的。

void TestBenchmark::multiple_data()
 {
     QTest::addColumn<bool>("useLocaleCompare");
     QTest::newRow("locale aware compare") << true;
     QTest::newRow("standard compare") << false;
 }

測試函數使用輸入數據決定什麽被基準測試。

void TestBenchmark::multiple()
 {
     QFETCH(bool, useLocaleCompare);
     QString str1 = QLatin1String("This is a test string");
     QString str2 = QLatin1String("This is a test string");

     int result;
     if (useLocaleCompare) 
     {
         QBENCHMARK {
             result = str1.localeAwareCompare(str2);
         }
     } 
     else 
     {
         QBENCHMARK {
             result = (str1 == str2);
         }
     }
 }

“if(useLocaleCompare)”開關放在QBENCHMARK宏外部避免測量開銷。每個基準測試函數可以有一個在用的QBENCHMARK宏。

七、註意事項

A、單元測試類中建議不要出現私有成員,尤其是指針,同時不建議在測試函數中建立被測類的指針,而是直接建立被測類的對象,在測試結束後容易遺忘指針。若需要指針,在initTestCase函數中new,在cleanupTestCase函數中delete。
B、若某個測試函數中出現了new,一定記著delete,且務必讓delete在第一個斷言前出現,因為斷言失敗函數就回立刻結束,並把當前函數標記為測試失敗。若delete在第一個斷言之後,而第一個斷言失敗則不會執行之後的delete。
C、若測試類必須有私有成員,必須註意一個測試類中的所有函數公用私有成員,不會在每個測試之前刷新狀態。
D、若被測類為單例,欲對其內所有函數做單元測試,會出現測試第一個函數可以保證測試環境為初始狀態,後續測試會因為單例的原因,導致測試時建立在之前操作後的環境下。欲解決此問題,需要刪除單例。

Qt高級——QTestLib單元測試框架