1. 引言
最近寫python用unittest模塊做單元測試,才發現自己過去寫C++居然都是手工測試。查了一番資料之后,發現Catch和Valgrind這兩個工具可以很好地滿足需求。
測試C++程序時,我們通常會在意兩件事:
- 運行結果是否正確?
- 是否發生了內存泄漏?
第一件事所有編程語言都需要在意,通常是給程序各種輸入,檢驗輸出的正確性,Catch是一個輕巧的單元測試框架,學習起來非常容易;
第二件事應該是C/C++獨有的,需要跟蹤運行時動態分配的內存,雖然可以自行重載new/delete運算符做到這一點,但Valgrind可以為我們檢測絕大多數內存相關問題(包括內存泄漏、數組越界、使用未初始化變量等)。
2 準備工作
2.1 環境
我用的系統是Ubuntu 16.04,之所以推薦這兩款工具,是因為它們安裝和使用都太容易了,完全不折騰。
首先不妨觀摩下怎樣搭建Gtest環境,然后我們來安裝Catch。
第一步,下載Catch的單一頭文件Catch.hpp;
第二步,把Catch.hpp放到工程目錄下(確保能正確include即可)。
結束了,比把大象裝到冰箱里還少一步。
然后安裝Valgrind只要一步:
apt-get install valgrind
2.2 編寫Trie
正好刷Leetcode寫到了Trie,就修改一下拿它做例子,不感興趣可以直接跳到第三節Catch的使用方法。
Trie又叫字典樹、前綴樹(prefix tree),是一種用來實現快速檢索的多叉樹。簡單地講,從根節點出發經過的路徑確定了一個字符串,每個節點有標記當前字符串是否為有效單詞。比如記當前字符串為s,走ten那條路徑的話:
- 選擇"t",s從“”變成“t”,不是有效單詞;
- 選擇“e”,s從“t”變成“te”,不是有效單詞;
- 選擇“n”,s從“te”變成“ten”,是有效單詞。
首先來看Trie中節點TrieNode的定義:
typedef struct TrieNode {
bool completed;
std::map<char, TrieNode *> children;
TrieNode() : completed(false) {};
} TrieNode;
TrieNode用bool值completed標記當前字符串是否為有效單詞,用children實現字符到后繼TrieNode的映射。比如上圖的根節點,children里就會有“t”、“A”、“i”三項。
然后來看Trie的定義:
class Trie {
public:
Trie(void);
~Trie(void);
void insert(std::string word);
bool search(std::string word);
private:
TrieNode *root;
};
含義非常清楚,除去構造和析構函數外,insert用于把單詞加入Trie,search用于查找單詞是否在Trie中。
Trie的具體實現放在我github上的DSAF里,這里直奔主題不再贅述。
3. 使用Catch
話不多說,直接看使用Catch的測試文件test.cpp:
#include <iostream>
#include <cstdlib>
#include "Trie.h"
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
using namespace std;
TEST_CASE("Testing Trie") {
// set up
Trie *t = new Trie();
// different sections
SECTION("Search an existent word.") {
string word = "abandon";
t->insert(word);
REQUIRE(t->search(word) == true);
}
SECTION("Search a nonexistent word.") {
string word = "abandon";
REQUIRE(t->search(word) == false);
}
// tear down
delete t;
}
除去trivial的#include "catch.hpp"外,使用#define CATCH_CONFIG_MAIN表示讓Catch自動提供main函數,運行在TEST_CASE中設計的測試。
簡單地講,每個TEST_CASE由三部分組成,set up、sections和tear down,set up是各個section都需要的準備工作,tear down是各個section都需要的清理工作,set up和tear down對于每個section都會執行一遍。
比如有一個TEST_CASE:
TEST_CASE {
set up
case 1
case 2
tear down
}
真正執行時就是:set up->case 1->tear down->set up->case 2->tear down。
此處TEST_CASE里的兩個section,第一個section是查找Trie中存在的單詞,第二個section是查找Trie中不存在的單詞。REQUIRE是Catch提供的宏,相當于assert,檢驗表達式是否成立。
寫好Makefile文件:
HDRS = $(wildcard *.h)
SRCS = $(wildcard *.cpp)
OBJS = $(patsubst %.cpp, %.o, $(SRCS))
DEPS = $(patsubst %.cpp, %.d, $(SRCS))
TARGET = test
CXX = g++
$(TARGET): $(OBJS)
$(CXX) -g -o $(TARGET) $(OBJS)
-include $(DEPS)
%.o: %.cpp
$(CXX) -c -MMD -std=c++11 $<
.PHONY: clean
clean:
-rm *.o
-rm *.d
-rm *.gch
-rm $(TARGET)
make之后運行test,可以看到:
修改search方法,使得總是返回true,那么第一個section仍然正確,第二個section出錯:
Catch告訴我們在第二個section,也就是“Search a nonexistent word”時出錯,失敗的原因是實際search結果為true。
4. 使用Valgrind
Valgrind其實是一套工具的集合,可以用--tool參數指定使用哪種工具,默認使用的是內存檢測工具Memcheck。Valgrind使用更加簡單,比如編譯鏈接后的可執行文件是test,那么檢測內存泄漏情況只需使用命令:
valgrind leak-check=yes ./test
注意要用./test而不是test。
注釋掉test.cpp里tear down部分,那么構造的Trie不會被釋放,用Catch測試仍然會通過:
用Valgrind檢測會得到很長的報告,這里只看最后的leak summary:
definitely lost和indirectly lost的含義可以看參考資料,這兩個值不為0表示發生了內存泄漏。
恢復tear down部分,再次make后用Valgrind檢測:
definitely lost和indirectly lost都為0,沒有內存泄漏。
說起來Valgrind真的非常厲害,比如寫了這種毫無違和感的錯誤代碼:
int main() {
int *d = new int[18];
delete d;
return 0;
}
真心不一定能看出錯誤,但用Valgrind檢測一下,就會在報告里看到:
提示釋放方法錯誤(mismatched free),應該用delete []。
5. 參考資料
- Catch的官方tutorial:
https://github.com/philsquared/Catch/blob/master/docs/tutorial.md - Valgrind的官方quick start guide:
http://valgrind.org/docs/manual/quick-start.html#quick-start.mcrun - 使用Valgrind memcheck進行C/C++的內存泄漏檢測:
http://www.oschina.net/translate/valgrind-memcheck - 應用 Valgrind 發現 Linux 程序的內存問題:
https://www.ibm.com/developerworks/cn/linux/l-cn-valgrind/