測試C++程序:使用Catch和Valgrind

1. 引言

最近寫python用unittest模塊做單元測試,才發現自己過去寫C++居然都是手工測試。查了一番資料之后,發現Catch和Valgrind這兩個工具可以很好地滿足需求。

測試C++程序時,我們通常會在意兩件事:

  1. 運行結果是否正確?
  2. 是否發生了內存泄漏?

第一件事所有編程語言都需要在意,通常是給程序各種輸入,檢驗輸出的正確性,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那條路徑的話:

  1. 選擇"t",s從“”變成“t”,不是有效單詞;
  2. 選擇“e”,s從“t”變成“te”,不是有效單詞;
  3. 選擇“n”,s從“te”變成“ten”,是有效單詞。
Trie示意圖,盜自wiki

首先來看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出錯:

search總是返回true

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測試仍然會通過:

注釋掉tear down部分

用Valgrind檢測會得到很長的報告,這里只看最后的leak summary:

未釋放的leak summary

definitely lost和indirectly lost的含義可以看參考資料,這兩個值不為0表示發生了內存泄漏。

恢復tear down部分,再次make后用Valgrind檢測:

釋放后的leak summary

definitely lost和indirectly lost都為0,沒有內存泄漏。

說起來Valgrind真的非常厲害,比如寫了這種毫無違和感的錯誤代碼:

int main() {
    int *d = new int[18];
    delete d;
    return 0;
}

真心不一定能看出錯誤,但用Valgrind檢測一下,就會在報告里看到:

mismatched free

提示釋放方法錯誤(mismatched free),應該用delete []。

新技能get√

5. 參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,837評論 18 139
  • 1.1 什么是自動引用計數 概念:在 LLVM 編譯器中設置 ARC(Automaitc Reference Co...
    __silhouette閱讀 5,235評論 1 17
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,814評論 25 708
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,739評論 18 399
  • 生產/采購入庫后,入庫的數量會自動加入庫存量。現在咱們回到《訂單和出貨》頁面。注意到,某些訂單項經過入庫,其庫存量...
    a85d6aa7027f閱讀 292評論 0 1