GTest基础学习-03-第2个单元测试-类级的单元测试

继续学习gtest框架中自带的单元测试范例,前面第一个单元测试是基于函数级别,主要就是测试函数内部的逻辑覆盖,函数内部读个分支建议都设计单元测试去覆盖到。主要学习到TEST这个宏,里面有两个参数,第一个参数是测试名称,第二个参数是测试用例名称。gtest这里测试名称和测试用例名称确实有点概念绕,然后学习到了EXPECT_EQ(预期结果,实际结果), 这个断言宏如果发生失败,会在控制台打印输出预期结果和实际结果,方便调试修改测试代码。

 

1.测试名称和测试用例名称

在GTest宏中,一个基本的单元测试空壳是这样的

TEST(测试名称,测试用例名称)
{
    // 测试代码
    // 测试断言
}

这个概念和我们在实际测试中其实不太一样,不好去一一对应起来。我从测试角度来解释这两个概念:

1.测试名称,这里指的是一个比较笼统的测试范围,例如前面一篇TEST(FactorialTest, Negative)这里的测试名称是FactorialTest,也就是阶乘测试,相对是宽泛笼统的测试范围,你可以理解为这个测试名称就是一个测试组的概念。

误区:我看有些人因为项目中习惯在测试名称这里直接使用C++类或者API的名称,他就认为这个测试名称只能写类的名称,或者类名称前面或者后面带TEST,这是不对的,这可以随便写名称,但是注意名称中不能包含下划线符号。

项目中第一个参数测试名称一般确实是API或者class的名称,而且对应物理磁盘中文件夹的名称也是和第一个参数相同或者有对应关系。分不同的文件夹就是方便归类整理成败上千的gtest测试cpp文件。

 

2.测试用例名称,这个测试用例相对测试名称要粒度要小,可以直观告诉别人你这个测试是要测试什么。可以由三五个或七八个单词连接在一起形成一个短语,告诉别人你这个测试是干嘛,例如手动编写测试用例中的用例标题。

这个可以和测试用例设计关联起来,我们知道测试用例方法常见的就是边界判断和等价划分两种编写测试用例的方法。上面测试名称有点像等价划分之后的分支,测试用例名称就对应等价划分之后的一个个具体测试点。

 

2.第二个类级别的单元测试例子

以下代码cpp文件在我的vs工程中都去除了预编译头选项。

sample.h代码


#ifndef GTEST_SAMPLES_SAMPLE02_H_
#define GTEST_SAMPLES_SAMPLE02_H_

#include <string.h>


// 一个简单的字符串类
class MyString {
private:
	const char* c_string_;
	const MyString& operator=(const MyString& rhs);

public:
	// 使用new分配内存,克隆一个C风格的字符串(以0字符结尾)
	static const char* CloneCString(const char* a_c_string);

	// 默认构造一个Null字符串
	MyString() : c_string_(nullptr) {}

	// 通过克隆一个C语言风格的字符串构造一个MyString对象
	explicit MyString(const char* a_c_string) : c_string_(nullptr) {
		Set(a_c_string);
	}

	// 拷贝构造
	MyString(const MyString& string) : c_string_(nullptr) {
		Set(string.c_string_);
	}

	
	// 析构函数
	// 这里不需要使用纯虚析构
	~MyString() { delete[] c_string_; }

	// Gets 函数
	const char* c_string() const { return c_string_; }

	size_t Length() const { return c_string_ == nullptr ? 0 : strlen(c_string_); }

	// Sets 函数
	void Set(const char* c_string);
};


#endif  // GTEST_SAMPLES_SAMPLE02_H_#pragma once

上面说明一下一个感觉奇怪的地方

MyString() : c_string_(nullptr) {}

在构造函数中后面通过一个冒号跟着成员变量,这个是C++的特性语法,作用就是在构造函数的时候初始化参数列表,这里当前的作用就是在空参构造的时候初始化c_sring_的值为nullptr。

 

sample02.cpp

#include "sample02.h"
#include <string.h>

// 使用new分配内存,克隆一个C风格的字符串(以0字符结尾)
const char* MyString::CloneCString(const char* a_c_string) {
	// 一般来说,这个字符串是null的时候,我们经常忽略,没有考虑到
	if (a_c_string == nullptr) return nullptr;
	// strlen 是一个string类提供的api,作用是计算字符串的长度
	const size_t len = strlen(a_c_string);
	//c语法的字符串和C++ string在结尾处不一样,风格在最后以0符号作为字符串结束标记,为了存储符号0,这里需要+1
	char* const clone = new char[len + 1];
	//memcpy是微软在VC环境下一个vcruntime_string.h下的api,作用是内存级别的拷贝
	memcpy(clone, a_c_string, len + 1);
	return clone;
}

// Set方法
void MyString::Set(const char* a_c_string) {
	// 当 c_string == c_string_ 确保也能 工作
	const char* const temp = MyString::CloneCString(a_c_string);
	//delete[]是释放内存的,一般来说是有new[]出来的对象,需要用delete[]逐个释放内存
	delete[] c_string_;
	//思路就是,把原字符串所指向的内存释放,用一个新的temp字符串对象赋值给c_string
	c_string_ = temp;
}

 

TestSample02.cpp

// 这个例子来演示如何针对一个类写更复杂一些测试
// 通常这个类有几个成员函数
//通常来说,在一个类中,给每个方法都写一个测试是一个好主意
// 你可以不必这样做,但是这样做可以帮助你管理你的测试
// 你还可以进行其他测试,如有需要

#include "sample02.h"
#include "gtest/gtest.h"
namespace {
	// 在这个例子中, 我们用一个简单的字符串来测试下 MyString 类 

	// 测试默认构造函数
	TEST(MyString, DefaultConstructor) {
		const MyString s;

		// 断言 s.c_string() 返回 NULL.

		//如果这里我们用NULL来替代static_cast<const char *>(NULL)
		//   
		//在这个断言中, 在gcc 3.4编译器上将产生一个警告. 
		//原因是EXPECT_EQ宏需要知道参数的类型以便当断言失败的时候打印参数出来
		//自设计依赖NULL是定义为0,编译器将使用格式化函数去打印int
		// 然而gcc认为NULL应该被用作一个指针,并应该是int,因此给出警告
		//
		// 这个问题的根本原因是C++缺少区分整形数字0和null指针常量,很不幸,我们必须接受这个事实

		EXPECT_STREQ(nullptr, s.c_string());

		EXPECT_EQ(0u, s.Length());
	}

	const char kHelloString[] = "Hello, world!";

	// 测试带参构造,参数是一个c语言字符串
	TEST(MyString, ConstructorFromCString) {
		const MyString s(kHelloString);
		EXPECT_EQ(0, strcmp(s.c_string(), kHelloString));
		EXPECT_EQ(sizeof(kHelloString) / sizeof(kHelloString[0]) - 1,
			s.Length());
	}

	// 测试拷贝构造
	TEST(MyString, CopyConstructor) {
		const MyString s1(kHelloString);
		const MyString s2 = s1;
		EXPECT_EQ(0, strcmp(s2.c_string(), kHelloString));
	}

	// 测试Set函数
	TEST(MyString, Set) {
		MyString s;

		s.Set(kHelloString);
		EXPECT_EQ(0, strcmp(s.c_string(), kHelloString));

		// Set函数支持输入前后两个一样的对象
		//
		s.Set(s.c_string());
		EXPECT_EQ(0, strcmp(s.c_string(), kHelloString));

		// 这里可以把MyString设置为NULL吗?
		s.Set(nullptr);
		EXPECT_STREQ(nullptr, s.c_string());
	}
}  // namespace

加上前面sample01的单元测试,一起运行结果如下

这个输出的格式用到了TEST宏的测试名称和测试用例名称,中间就点隔开。观察下面倒数第二行

10 tests from 3 test suites ran。

一共10个测试用例,放在3个test suites中,在java的Jnuit测试框架也有这个test suites概念。中文名称是测试套件,严格来说和测试组是不同的概念。一个测试套件里面包含多个测试用例。这里测试套件名称就是TEST宏的第一个参数也就是官方文档所说的测试名称。

 

3.总结下目前所接触过的断言宏

EXPECT_EQ(1, 1);		// EQ是equals的意思,判断两个对象是否相等
EXPECT_GT(2, 0);		// GT是greater then, 比 xx更大,通过是数值比较
EXPECT_FALSE( 3>5);	        // 断言结果是false,参数是返回类型bool的表达式或函数
EXPECT_TRUE( 3>1);		// 断言结果是true,参数是返回类型bool的表达式或函数
EXPECT_STREQ("aaa", "aaa");	// STREQ 是string equals,判断两个字符串是否相等

在gtest框架中,还有很多断言宏,后面专门一篇来介绍不同的断言宏的基本使用和含义。

 

在看sample中的头文件和实现文件虽然都是英文,看起来也是有点意思,能学习到一些知识,例如这篇代码注释中说到,在g++编译器中会出现警告,就是由于C++无法区分数字null表示的0还是空指针。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值