目录

​引言​

​选择Catch的原因​

​使用方法​

​试用例子​

​使用Catch​

​catch有两种测试用例的书写方式​

​最佳实践​

​单元测试是什么,什么时候需要单元测试​

​单元测试分类​



引言

测试C++程序时,我们通常会在意两件事:

  1. 运行结果是否正确?
  2. 是否发生了内存泄漏?

第一件事所有编程语言都需要在意,通常是给程序各种输入,检验输出的正确性,Catch是一个轻巧的单元测试框架,学习起来非常容易;

第二件事应该是C/C++独有的,需要跟踪运行时动态分配的内存,虽然可以自行重载new/delete运算符做到这一点,但Valgrind可以为我们检测绝大多数内存相关问题(包括内存泄漏、数组越界、使用未初始化变量等)。

链接:https://www.jianshu.com/p/6f03a0cfe60c

 

选择Catch的原因

比gtest简单,单文件,少配置。

Catch 是一个很时尚的,C++原生的框架,用于单元测试,TDD测试驱动开发和BDD行为驱动开发,catch的好处是,

它只有一个头文件,坏处是,它需要C++11,不过不是很坏)

github地址:https://github.com/philsquared/Catch

git clone https://github.com/philsquared/Catch.git

使用方法

第一步,下载Catch的单一头文件​​Catch.hpp​​;

可以在github地址:https://github.com/philsquared/Catch

直接下载​​catch.hpp​​文件,引入到自己的c++工程中使用。

第二步,把Catch.hpp放到工程目录下(确保能正确include即可)。

 

试用例子

编写Trie

正好刷Leetcode写到了Trie,就修改一下拿它做例子,不感兴趣可以直接跳到第三节Catch的使用方法。

Trie又叫字典树、前缀树(prefix tree),是一种用来实现快速检索的多叉树。简单地讲,从根节点出发经过的路径确定了一个字符串,每个节点有标记当前字符串是否为有效单词。比如记当前字符串为s,走ten那条路径的话:

  1. 选择"t",s从“”变成“t”,不是有效单词;
  2. 选择“e”,s从“t”变成“te”,不是有效单词;
  3. 选择“n”,s从“te”变成“ten”,是有效单词。

 

【单元测试】C/C++单元测试Catch_c++

首先来看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​​里,这里直奔主题不再赘述。

使用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,可以看到:

【单元测试】C/C++单元测试Catch_#include_02

修改search方法,使得总是返回true,那么第一个section仍然正确,第二个section出错:

【单元测试】C/C++单元测试Catch_#include_03

Catch告诉我们在第二个section,也就是“Search a nonexistent word”时出错,失败的原因是实际search结果为true。

 

 

catch有两种测试用例的书写方式

Normal

unsigned int Factorial( unsigned int number ) 
{
return number < 1 ? 1 : Factorial(number-1)*number;
}

TEST_CASE( "Factorials are computed", "[factorial]" )
{
REQUIRE( Factorial(0) == 1 );
REQUIRE( Factorial(1) == 1 );
REQUIRE( Factorial(2) == 2 );
REQUIRE( Factorial(3) == 6 );
REQUIRE( Factorial(10) == 3628800 );
}

BBD

SCENARIO, GIVEN, WHEN and THEN macros, which map on to TEST_CASEs and SECTIONs, respectively

SCENARIO( "vectors can be sized and resized", "[vector]" ) {

GIVEN( "A vector with some items" ) {
std::vector<int> v( 5 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );

WHEN( "the size is increased" ) {
v.resize( 10 );

THEN( "the size and capacity change" ) {
REQUIRE( v.size() == 10 );
REQUIRE( v.capacity() >= 10 );
}
}
WHEN( "the size is reduced" ) {
v.resize( 0 );

THEN( "the size changes but not capacity" ) {
REQUIRE( v.size() == 0 );
REQUIRE( v.capacity() >= 5 );
}
}
WHEN( "more capacity is reserved" ) {
v.reserve( 10 );

THEN( "the capacity changes but not the size" ) {
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 10 );
}
}
WHEN( "less capacity is reserved" ) {
v.reserve( 0 );

THEN( "neither size nor capacity are changed" ) {
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );
}
}
}
}

最佳实践

将catch宏和头文件用一个单独的main文件包含,避免编译时的时间浪费;

// tests-main.cpp
#define CATCH_CONFIG_MAIN
#include "catch.hpp"

// tests-factorial.cpp
#include "catch.hpp"

#include "factorial.h"

TEST_CASE( "Factorials are computed", "[factorial]" ) {
REQUIRE( Factorial(1) == 1 );
REQUIRE( Factorial(2) == 2 );
REQUIRE( Factorial(3) == 6 );
REQUIRE( Factorial(10) == 3628800 );
}

 

单元测试是什么,什么时候需要单元测试

单元测试是什么

 

什么时候需要单元测试

不需要:

小的代码量,简单固定的需求,个人开发,一锤子买卖等等都会让单元测试显得不那么重要。

需要:

较大的项目,项目的需求很多,一直在开发/修改,遇到了这样的痛苦状况:

1.客户总能在使用中找出BUG,

2.每次代码的改动,都会导致一些意想不到的BUG出现。

这个时候,单元测试可以挽救你。

记住,单元测试的威力更多不是体现在新代码的编写上,而是对已有代码的更改

既:

增量还是存量

  • 单测case针对增量代码
  • 当存量代码出现大规模重构,后者质量暴露出极大风险时,都是推动补全单测的好时机

 

单元测试​​​​​​​代码多码

事实上单元测试代码都是异常简单的一些“断言”代码,断言就是判断一个函数或对象的一个方法所产生的结果是否等于你期望的那个结果,这样的代码看起来很多,但事实上书写的成本很低。

 

单元测试分类

(链接:https://www.zhihu.com/question/28729261/answer/1058317111)

小型测试中型测试大型测试

  • 小型测试,针对单个函数的测试,关注其内部逻辑,mock所有需要的服务。小型测试带来优秀的代码质量、良好的异常处理、优雅的错误报告
  • 中型测试,验证两个或多个制定的模块应用之间的交互
  • 大型测试,也被称为“系统测试”或“端到端测试”。大型测试在一个较高层次上运行,验证系统作为一个整体是如何工作的。

【单元测试】C/C++单元测试Catch_单元测试_04

【单元测试】C/C++单元测试Catch_字符串_05


结论:我们的单元测试,既可以针对一个函数写case,也可以按照函数的调用关系串起来写case。 金字塔模型