朱健



摘要: Qt是跨平臺的應用軟件開發框架,Qt Test是Qt框架的單元測試庫?;赒t Test的單元測試可以進行功能和性能測試。持續集成系統可以自動化單元測試的構建、部署、運行和結果統計過程。工程實踐表明:基于Qt Test的單元測試與持續集成系統的結合可以降低軟件的缺陷率,優化軟件架構的設計,提高軟件工程的自動化。
Abstract: Qt is a cross platform application development framework. Qt Test is the unit testing library provided by Qt framework. The Qt Test based unit testing can be used for functionality and benchmarking testing. Continuous integration system can automate the build, deployment, run and result statistic processes of unit testing. The engineering practice shows the combination of Qt Test based unit testing and continuous integration system can be used for decreasing the defect rate of software, optimizing the design of software architecture, improving the automation of software engineering.
關鍵詞: 單元測試;Qt;持續集成;自動化
Key words: unit testing;Qt;continuous integration;automation
中圖分類號:TP31 文獻標識碼:A 文章編號:1006-4311(2017)14-0216-04
0 引言
基于Qt框架的大型應用軟件通常包括用戶界面,業務邏輯,工具庫,UI組件庫,平臺適配層等,軟件規模大,邏輯復雜,通常進行層次化和模塊化設計,軟件的整體穩定性和效率與每個基礎模塊的代碼質量息息相關。單元測試是對軟件模塊的功能測試,可及早發現軟件設計和實現中的問題,便于問題的定位[2]。為了降低軟件的缺陷率,業界提出并實施了測試驅動開發(Test Driven Development)方法,該方法主張先開發單元測試程序,由單元測試結果驅動軟件的設計和編碼,以到達更具針對性的架構設計和更低的首次發布缺陷率[6,7]。所以單元測試是大型尤其是采用測試驅動開發的Qt軟件工程的必然要求。
在工程實踐中常用的通用單元測試框架包括:適用于C++語言的CppUnit[3],適用于Java語言的JUnit[2]等。但是基于Qt的軟件大都依賴于Qt對C++語言的擴展,如信號-槽(signal-slot)機制,屬性(property)機制,元對象(meta-object)框架等;構建過程依賴于Qt特有的工具鏈(如qmake,moc等);運行過程依賴于Qt特有的基礎類庫,所以很難使用CppUnit等通用單元測試框架對基于Qt的軟件進行單元測試。而Qt Test是Qt框架的單元測試模塊,可以與Qt的工具鏈、基礎類庫、應用框架、集成開發環境(IDE)等良好結合[5]。
1 Qt Test單元測試框架分析
1.1 Qt Test單元測試原理
在Qt Test單元測試體系中,每個測試文件是一個測試集合,測試集合是一個繼承自QObject的測試對象(Test Object),測試對象的每個私有槽(private slot)都是一個測試函數。Qt Test單元測試框架通過QObject::metaObject()獲取測試對象的QMetaObject屬性,通過QMetaObject::method()依次獲取測試對象的私有槽,逐個調用并輸出測試結果。一個典型的測試對象可以用如下的類定義:
class TestSuite : public QObject {
Q_OBJECT
public:
TestSuite();
private Q_SLOTS:
void initTestCase();
void cleanupTestCase();
void testcase1();
void testcase2();
…
};
其中initTestCase()和cleanupTestCase()是兩個特殊的測試函數,initTestCase()在測試程序開始運行時調用,主要用于準備測試環境,包括構造測試相關的對象、樁對象、其他基礎Qt庫對象,初始化局部變量,進行數據庫連接,準備測試數據等。cleanupTestCase()在測試程序結束運行前調用,是initTestCase()的逆過程,用于銷毀測試過程中構造的對象資源,刪除測試運行過程中生成的數據等。其余的私有槽都是獨立測試函數,測試函數的實現需要根據測試例流程進行一系列的操作,驗證功能和性能要求是否達到預期。
每個Qt Test測試文件最終構建成一個單元測試程序,單元測試程序可以獨立運行,不需要運行工具(Test Runner),可以在資源受限的嵌入式Linux設備上運行。單元測試的運行流程如圖1:測試程序啟動后,測試框架通過QTest::qExec()運行測試對象。首先,測試對象的initTestCase()被調用;之后測試對象的各個測試函數會被依次調用,并記錄測試結果;最后cleanupTestCase()被調用,測試結果輸出,單元測試結束。
1.2 Qt Test單元測試內容
1.2.1 功能性測試
單元測試的基本功能是進行功能性的測試,通過調用待測對象的接口,完成測試例規定的流程,比對期望值和實際值,并輸出測試結果。Qt Test提供一系列宏,用于在測試代碼中比對期望值與實際值。例如,QVERIFY(expr)用于校驗二元表達式expr,若expr為假(false)則判定當前測試例失??;QCOMPARE(value1, value2)用于判斷兩個值value1,value2是否相等,若不相等則判定測試例失敗。
在功能性覆蓋率達到100%的情況下,需要進一步提高代碼路徑覆蓋率。為提高單個測試例函數的代碼路徑覆蓋率,可以采用數據驅動測試(Data Driven Test)方法:即設計多組數據,多次運行該測試例函數,以提高測試效率??筛鶕郎y模塊的代碼實現、測試例的需求規約,設計出覆蓋邊界條件、錯誤處理分支的測試數據集,提高單元測試的代碼路徑覆蓋率。Qt Test支持數據驅動測試:實現以"_data"為結尾的測試函數為同名測試函數提供測試數據,測試代碼通過QFETCH獲取測試數據。以下代碼構造了{20, 23, "Type A"}, {2000, 2098, "Type B"}兩組數據對代碼中的算法Classifier::doClassifier()進行了測試。
void TestSuite::testcase1_data() {
QTest::addColumn
QTest::addColumn
QTest::addColumn
// 第一組測試數據
QTest::newRow << 20 << 23 << QString("Type A");
// 第二組測試數據
QTest::newRow << 2000 << 2098 << QString("Type B");
}
void TestSuite::testcase1() {
// 獲取數據并測試
QFETCH(int, inputX);
QFETCH(int, inputY)
QFETCH(QString, result);
QString type = Classifier::doClassify(inputX, inputY);
QVERIFY(type == result);
}
1.2.2 GUI測試
通常的GUI測試主要是黑盒測試,通過模擬鍵盤、鼠標事件驅動GUI應用程序運行,捕捉屏幕上的GUI對象比對測試結果,需要復雜的Test Runner支持[1],而且這類GUI測試不能對單個GUI控件進行單元測試。Qt Test為繼承自QWidget的控件提供了GUI單元測試功能:使用QTest::keyClick(),QTest::mouseClick()模擬鍵盤和鼠標事件驅動控件運行;通過QVERIFY,QCOMPARE比對期望值和實際結果,不需要復雜的對象捕捉技術。GUI測試同樣支持數據驅動的測試,QTestEventList用于定義作用在QWidget上的事件序列,例如:
QTestEventList events;
CustomWidget w;
// 定義事件序列
events.addMouseClick(Qt::LeftButton, 0, QPoint(20, 30));
events.addKeyClick(Qt::Key_Backspace);
events.addDelay(100);
// 模擬運行事件序列
events.simulate(&w);
// 比對測試結果
QCOMPARE(w.myProperty(), expectedValue);
Qt Test的GUI測試不依賴任何測試工具,以輕量級方式模擬鼠標、鍵盤事件,便于團隊在設計和實現GUI組件時做充分的測試,避免系統集成時因GUI組件缺陷導致系統異常。
1.2.3 性能測試
軟件的整體性能與基礎模塊的性能密切相關,只對基礎模塊做功能性單元測試,不能保證其性能指標。Qt Test可以使用QBENCHMARK進行性能測試。QBENCHMARK可以對塊內代碼片段的運行時間進行測定。單元測試運行時,QBENCHMARK塊內的代碼片段會被運行多次并記錄運行時間,從而得到比較精確的結果。性能測試的反復運行次數,性能計量單位,最小可接受的性能值等可以通過單元測試命令行參數設定。在單元測試中引入性能測試,可以在設計和實現階段及時發現性能問題,避免軟件集成時運行效率無法達標的問題;在軟件維護階段,基礎模塊的性能測試也可以及時定位不當修改引入的性能問題,保證軟件質量平穩。
1.3 Qt Test單元測試的改進
單元測試過程應該由大到小,劃分為若干測試模塊,最終細化到函數級別,在函數級別完成邏輯性測試,覆蓋代碼分支,再由小到大完成集成測試和功能性測試[2]。從代碼分支到系統功能的充分測試,可以及時發現系統中存在的問題,提高測試效率,降低開發后期的測試和維護成本。軟件的設計和實現需要遵循如下原則:①將復雜的大模塊拆分成適合于單元測試的小模塊。對每個小模塊進行單獨的單元測試,有利于缺陷的定位和排除。②將業務邏輯與框架做分離。軟件設計時應該避免將業務邏輯與框架耦合在一起,否則會造成無法通過構造模擬數據進行單元測試。③基礎模塊的單個函數不能實現過多功能,復雜的函數會造成測試粒度過大,不利于缺陷定位,復雜函數應拆分為較小的函數。④消除冗余代碼。冗余代碼會增加單元測試的設計難度,降低測試效率,代碼實現時需要消除冗余變量、冗余接口,提取冗余代碼的公共部分為獨立的函數。
2 自動化Qt Test單元測試方法
2.1 自動化Qt Test單元測試的流程和工具
大型Qt應用程序通常包含大量子模塊,本身需要開發大量的單元測試程序進行測試;在測試驅動的開發中,需要先設計和實現單元測試用例,再進行功能模塊的開發,隨著開發的迭代,單元測試工程數量必然逐步增多。所以,在實際項目中,單元測試工程的開發和維護工作量是很大的。為了提高單元測試的開發和維護效率,可以使用Qt Creator集成開發環境(IDE)。Qt Creator可以將多個的單元測試工程以子工程(sub project)的形式進行管理,有利于單元測試工程的分類和維護;新建單元測試工程時,Qt Creator可以為開發者生成框架代碼,避免重復勞動。
隨著單元測試程序數量增加,由開發者手動運行每個單元測試程序并查看結果將是繁重的重復性勞動,且不能對單元測試中的警告、錯誤、失敗率等進行有效的統計和歸檔,項目管理者也無法及時了解單元測試的進度、狀況等。持續集成(Continuous Integration)系統可以自動化單元測試的構建、部署、運行、結果統計過程。持續集成是一種軟件工程實踐,即通過工具自動從源代碼管理系統獲取項目的源代碼,自動編譯,部署,運行軟件。持續集成能夠及時發現和解決軟件工程中存在的問題,減輕開發者的日常工作量,為項目管理者提供可視化的數據參考,Jenkins[4]是目前業內廣泛使用的持續集成系統。Qt Creator和Jenkins系統相結合可以有效提高Qt Test單元測試各步驟的自動化程度(如圖2),提高Qt Test單元測試的效率,實現基于Qt Test的測試驅動開發。
2.2 使用Qt Creator開發和維護Qt Test單元測試工程
Qt Creator是Qt默認的集成開發環境(IDE),使用Qt Creator工程向導可以快速創建Qt Test單元測試工程。可以通過“文件->新建文件或項目->其他項目->Qt單元測試”打開“Qt單元測試”向導(圖3)。
在Qt單元測試向導分為5個步驟:位置,構建套件(Kit),模塊,詳情,匯總,“位置”用于設置單元測試工程路徑,“構建套件”用于指定目標代碼的編譯器或交叉編譯器,“模塊”用于添加單元測試工程依賴的Qt基礎類庫,詳情用于生成單元測試框架代碼。向導生成單元測試工程的框架代碼,開發者只需進一步增加和完善測試代碼。開發者可以在Qt Creator中編譯和運行單元測試代碼,最終完成單元測試工程的開發。
2.3 使用Jenkins自動化構建、部署、結果統計
在Jenkins系統Web界面的“項目->配置->構建”頁面可編寫自動化構建、部署、運行單元測試程序的腳本,在Linux環境下采用Shell腳本編寫?!绊椖?>配置->構建后操作”頁面可以添加“Publish xUnit test report”步驟用于呈現Qt單元測試程序生成的基于XML的測試結果。
以自動化嵌入式Linux設備的單元測試為例,首先需要在Jenkins編譯主機上交叉編譯生成可執行的Qt單元測試程序,對于支持輕量級SSH服務如dropbear的Linux設備,可以采用SSH(Secure Shell)協議進行單元測試程序的部署、運行及結果回收。以下是自動化腳本的主要代碼:
# 編譯單元測試程序
mkdir $WORKSPACE/build
mkdir $WORKSPACE/targetfs
cd build
qmake ..
make INSTALL_ROOT="$WORKSPACE/targetfs" install
cd $WORKSPACE/targetfs/
# 將單元測試程序部署到遠程設備
scp tst_* root@
# 在遠程設備上運行單元測試程序
ssh root@
# 將單元測試結果收回本地
scp root@
此例中,單元測試程序可執行文件名以tst_開頭,remote_run.sh位于待測Linux設備上,用于批量運行單元測試程序并輸出XML格式的測試結果,remote_run.sh的核心代碼如下:
# 枚舉單元測試程序
for unittest in tst_*
do
# 執行單元測試程序,并導出XML格式的測試結果
bash -c "./${unittest} -xml -o report_{unittest}.xml"
done
Jenkins的“xUnit test report”插件可以根據XML格式的Qt Test測試結果以測試結果趨勢圖(圖4)、詳細測試結果表格(圖5)的形式呈現出來。測試結果趨勢圖頁面可以形象地展現近期單元測試總數、通過率、失敗率的變化趨勢;詳細測試結果表格頁面展示所有測試例的結果詳情,包括失敗測試例的失敗信息、各測試例的運行結果和執行時間等。
3 結論
在測試驅動的開發中,需要先完成單元測試代碼,根據單元測試結果迭代式地設計、實現、改進目標軟件,最終交付軟件產品。這種以單元測試為基礎的軟件開發模型,可以對軟件基礎模塊的基本功能、關鍵性能等做充分的測試,可以盡早發現、定位、解決軟件缺陷,改善軟件質量,優化軟件架構。
隨著軟件規模的擴大,單元測試的規模會逐步增大,構建、運行、檢查單元測試結果的工作量也隨之上升,通過持續集成系統進行自動化單元測試的實踐可以減輕開發人員的構建、部署、測試工作量,提高測試驅動開發的自動化程度,在持續集成系統中實時展示單元測試結果可以提高開發者定位、修正軟件缺陷的效率,可以為軟件工程的進度和質量提供定量化的數據參考。
參考文獻:
[1]吳立金,唐龍利,韓新宇,等.嵌入式軟件GUI自動化測試平臺研究[J].計算機測量與控制,2015,23(4):1094-1097.
[2]張敏,陳靜,王娟.基于MVC模式的Web系統自動化單元測試方案[J].微型電腦應用,2016,32(2):78-80.
[3]賈長偉,廖建,焉寧,等.基于CppUnit的虛擬試驗單元測試研究[J].計算機測量與控制,2015,23(4):1155-1160.
[4]John Ferguson Smart. Jenkins The Definitive Guide[M]. O'Reilly Media, 2011: 169-180.
[5]Qt Test user manual. http://doc.qt.io/qt-4.8/qtestlib-manual.html 2016.
[6]Nachiappan Nagappan, E. Michael Maximilien, Thirumalesh Bhat, Laurie Williams. Realizing quality improvement through test driven development: results and experiences of four industrial teams[J]. Empir Software Eng 2008,13: 289-302.
[7]Kent Beck. Test-Driven Development By Example[M]. Addison Wesley, 2002.