Bootstrap

GTest基础学习-06-第6个单元测试-接口测试(类型参数驱动)

       前面的文章学习了使用gtest进行函数级的测试和类级的测试,这篇来学习如何通过gtest来测试接口。这个接口我们在prime_table.h中,接口的头和实现部分都放置同一个文件,这个文件也在sample文件夹下。这篇学习第6个单元测试,介绍如何测试一个接口有多种实现,而且没种实现都要单元测试,做到测试代码不重复。本篇新引入了几个宏,有一些模板函数的思想,加上新的测试宏,看起来有费劲,不易理解和掌握。这个知识点比前面学习都要高级的特性和复杂。

 

1.代码准备

prime_table.h代码

#ifndef GTEST_SAMPLES_PRIME_TABLES_H_
#define GTEST_SAMPLES_PRIME_TABLES_H_

#include <algorithm>

// prime table 接口
class PrimeTable {
public:
	virtual ~PrimeTable() {}

	// 只有n是素数返回true
	virtual bool IsPrime(int n) const = 0;

	// 返回比P大的最小的素数
	// 如果下一个素数超出表的容量,就返回-1
	virtual int GetNextPrime(int p) const = 0;
};

// 实现 #1 实时计算素数
class OnTheFlyPrimeTable : public PrimeTable {
public:
	bool IsPrime(int n) const override {
		if (n <= 1) return false;

		for (int i = 2; i*i <= n; i++) {
			if ((n % i) == 0) return false;
		}

		return true;
	}

	int GetNextPrime(int p) const override {
		for (int n = p + 1; n > 0; n++) {
			if (IsPrime(n)) return n;
		}

		return -1;
	}
};

// 实现 #2 预计算素数并存储结果到一个数组
class PreCalculatedPrimeTable : public PrimeTable {
public:
	// max用来指定素数表最大数
	explicit PreCalculatedPrimeTable(int max)
		: is_prime_size_(max + 1), is_prime_(new bool[max + 1]) {
		CalculatePrimesUpTo(max);
	}
	~PreCalculatedPrimeTable() override { delete[] is_prime_; }

	bool IsPrime(int n) const override {
		return 0 <= n && n < is_prime_size_ && is_prime_[n];
	}

	int GetNextPrime(int p) const override {
		for (int n = p + 1; n < is_prime_size_; n++) {
			if (is_prime_[n]) return n;
		}

		return -1;
	}

private:
	void CalculatePrimesUpTo(int max) {
		::std::fill(is_prime_, is_prime_ + is_prime_size_, true);
		is_prime_[0] = is_prime_[1] = false;

		// 检查每一个候选的素数 (我们知道2是素数中唯一的偶数
		for (int i = 2; i*i <= max; i += i % 2 + 1) {
			if (!is_prime_[i]) continue;

			// 标记i不是素数
			// 我们从第i个乘数开始,因为所有较小的复数均已标记。
			for (int j = i*i; j <= max; j += i) {
				is_prime_[j] = false;
			}
		}
	}

	const int is_prime_size_;
	bool* const is_prime_;

	// 关闭编译器警告 "assignment operator could not be generated."
	void operator=(const PreCalculatedPrimeTable& rhs);
};

#endif  // GTEST_SAMPLES_PRIME_TABLES_H_

一个类下有两个接口,isPrime()判断是否是素数,GetNextPrime(int p)求比p要打且是素数中最新的数。这两个接口,给出两个实现方式。

TestSample06.cpp

//这示例展示了如何测试多个相同接口的实现,也叫接口测试

#include "prime_tables.h"
#include "gtest/gtest.h"
namespace {
	//首先,我们定义一些模板函数来创建实例实现。 
	//如果可以用相同的方式构造实现,你可跳过此步骤

	template <class T>
	PrimeTable* CreatePrimeTable();

	template <>
	// 这个时候,T就是实现OnTheFlyPrimeTable
	PrimeTable* CreatePrimeTable<OnTheFlyPrimeTable>() {
		return new OnTheFlyPrimeTable;
	}

	template <>
	// 这个时候,T就是实现PreCalculatedPrimeTable
	PrimeTable* CreatePrimeTable<PreCalculatedPrimeTable>() {
		return new PreCalculatedPrimeTable(10000);
	}

	// 定义一个测试夹具类模板
	template <class T>
	class PrimeTableTest : public testing::Test {
	protected:
		// 构造函数去调用模板函数去创建素数表
		PrimeTableTest() : table_(CreatePrimeTable<T>()) {}

		~PrimeTableTest() override { delete table_; }

		// 注意到我们测试一个实现是通过基本接口,而不是通过具体的实现类。
		// 这很重要使测试保持接近真实的场景,当通过基本接口调用实现
		PrimeTable* const table_;
	};

#if GTEST_HAS_TYPED_TEST

	using testing::Types;

	// Google Test 对于不同类型(Type)提供了两种方式去复用测试
	// 第一种叫 "typed tests".  
	//使用场景:如果你已经全部知道编写测试时要使用的参数类型

	// 为了编写类型化的测试用例,需要使用以下宏
	//   TYPED_TEST_SUITE(TestCaseName, TypeList); 去声明并指定参数类型
	// 如果使用 TEST_F宏, 参数TestCaseName必须和测试夹具类名称匹配

	// 我们想要测试的types列表
	typedef Types<OnTheFlyPrimeTable, PreCalculatedPrimeTable> Implementations;

	TYPED_TEST_SUITE(PrimeTableTest, Implementations);

	// 然后使用 TYPED_TEST(TestCaseName, TestName) 去定义一个typed 测试,和TEST_F相似
	TYPED_TEST(PrimeTableTest, ReturnsFalseForNonPrimes) {
		// 在test正文内部,你可以通过TypeParam去引用类型参数
		// 通过TestFixture引用测试夹具类,这个例子中我们不需要这样做
		
		// 由于我们处在模板类的世界中,因此C++明确要求
		//在引用测试类夹具成员是编写"this->"
		// 这是我们必须学习和遵守的东西
		EXPECT_FALSE(this->table_->IsPrime(-5));
		EXPECT_FALSE(this->table_->IsPrime(0));
		EXPECT_FALSE(this->table_->IsPrime(1));
		EXPECT_FALSE(this->table_->IsPrime(4));
		EXPECT_FALSE(this->table_->IsPrime(6));
		EXPECT_FALSE(this->table_->IsPrime(100));
	}

	TYPED_TEST(PrimeTableTest, ReturnsTrueForPrimes) {
		EXPECT_TRUE(this->table_->IsPrime(2));
		EXPECT_TRUE(this->table_->IsPrime(3));
		EXPECT_TRUE(this->table_->IsPrime(5));
		EXPECT_TRUE(this->table_->IsPrime(7));
		EXPECT_TRUE(this->table_->IsPrime(11));
		EXPECT_TRUE(this->table_->IsPrime(131));
	}

	TYPED_TEST(PrimeTableTest, CanGetNextPrime) {
		EXPECT_EQ(2, this->table_->GetNextPrime(0));
		EXPECT_EQ(3, this->table_->GetNextPrime(2));
		EXPECT_EQ(5, this->table_->GetNextPrime(3));
		EXPECT_EQ(7, this->table_->GetNextPrime(5));
		EXPECT_EQ(11, this->table_->GetNextPrime(7));
		EXPECT_EQ(131, this->table_->GetNextPrime(128));
	}

	//  gtest框架在TYPED_TEST_SUITE中指定的类型列表中将针对每种类型重复每个TYPED_TEST
	//  很高兴我们不用多次重复定义这些

#endif  // GTEST_HAS_TYPED_TEST

#if GTEST_HAS_TYPED_TEST_P

	using testing::Types;

	// 然而在一些时候,当你在写测试的时候你不知道全部的类型
	// 例如, 你是一个接口的作者,但是这个接口是由他人去写实现
	// 你可能会想写一组测试去确保每个实现都基本满足了需求
	// 但是你不知道这个接口的将来别人是怎么去写实现
	//
	// 没有知道的类型参数,你怎么去写测试用例呢
	// 这就是"type-parameterized tests"能够帮你做到的事情
	// 它比类型测试要复杂得多,但是作为回报,你会得到可以在许多情况下重用的测试模式
	// 接下来教你怎么做

	// 第一, 定义一个测试夹具类模板.  这里我们只需复用PrimeTableTest定义的夹具
	
	template <class T>
	class PrimeTableTest2 : public PrimeTableTest<T> {
	};

	// 第二, 声明这个测试用例.  参数名称就是测试夹具类名称
	// 通常也有测试名称这个参数.  这里 _P这个后缀代表 "parameterized" 或 "pattern"
	TYPED_TEST_SUITE_P(PrimeTableTest2);

	// 第三, 使用 TYPED_TEST_P(TestCaseName, TestName) 去定义一个测试
	// 和你在TEST_F做的差不多.
	TYPED_TEST_P(PrimeTableTest2, ReturnsFalseForNonPrimes) {
		EXPECT_FALSE(this->table_->IsPrime(-5));
		EXPECT_FALSE(this->table_->IsPrime(0));
		EXPECT_FALSE(this->table_->IsPrime(1));
		EXPECT_FALSE(this->table_->IsPrime(4));
		EXPECT_FALSE(this->table_->IsPrime(6));
		EXPECT_FALSE(this->table_->IsPrime(100));
	}

	TYPED_TEST_P(PrimeTableTest2, ReturnsTrueForPrimes) {
		EXPECT_TRUE(this->table_->IsPrime(2));
		EXPECT_TRUE(this->table_->IsPrime(3));
		EXPECT_TRUE(this->table_->IsPrime(5));
		EXPECT_TRUE(this->table_->IsPrime(7));
		EXPECT_TRUE(this->table_->IsPrime(11));
		EXPECT_TRUE(this->table_->IsPrime(131));
	}

	TYPED_TEST_P(PrimeTableTest2, CanGetNextPrime) {
		EXPECT_EQ(2, this->table_->GetNextPrime(0));
		EXPECT_EQ(3, this->table_->GetNextPrime(2));
		EXPECT_EQ(5, this->table_->GetNextPrime(3));
		EXPECT_EQ(7, this->table_->GetNextPrime(5));
		EXPECT_EQ(11, this->table_->GetNextPrime(7));
		EXPECT_EQ(131, this->table_->GetNextPrime(128));
	}

	// 类型参数化测试涉及一个额外的步骤:必须列举你定义的测试:
	REGISTER_TYPED_TEST_SUITE_P(
		PrimeTableTest2,  // 第一个参数是测试用例名称.
		// 其余的参数是测试名称,以下有3个测试名
		ReturnsFalseForNonPrimes, ReturnsTrueForPrimes, CanGetNextPrime);

	// 到这里测试模型完成.  然而, 你没有任何真正的测试,因为您还没有说出要运行的类型。
	// 要把抽象测试模型转换为真实的测试
	// 你需要用带有类型列表实例化
	// 通常测试模型都是在一个.h的头文件中定义,任何人可以通过#include包含进来和进行实例化
	// 你甚至可以在同一程序中序列化多次这个对象。
	// 为了区分不同的实例,请给每个实例起一个名称,成为测试用例名称的一部分,可以在测试过滤器中使用。

	//我们要测试的类型列表。 请注意,它不是在我们编写 TYPED_TEST_P()s.
	typedef Types<OnTheFlyPrimeTable, PreCalculatedPrimeTable>
		PrimeTableImplementations;
	INSTANTIATE_TYPED_TEST_SUITE_P(OnTheFlyAndPreCalculated,    // 实例名称
		PrimeTableTest2,             // 测试用例名称
		PrimeTableImplementations);  // 类型列表

#endif  // GTEST_HAS_TYPED_TEST_P
}  // namespace

建议大家先跟着注释读一篇代码或者多边代码。这个地方真的有点抽象和复杂,如果C++学习不是很好,理解起来会费劲,我确实没有完全理解,只看出了一个大概。

先看运行结果

结合输出看看有没有帮助理解类型参数测试的真谛。

 

3.新出现的宏

这里的类型参数宏测试的引入

// 我们想要测试的types列表
typedef Types<OnTheFlyPrimeTable, PreCalculatedPrimeTable> Implementations;

TYPED_TEST_SUITE(PrimeTableTest, Implementations);
TYPED_TEST(PrimeTableTest, ReturnsFalseForNonPrimes)

先定义一个Types,这种方式参数包含接口的两种全部实现,添加添加到宏TYPED_TEST_SUITE,有了类型参数之后,调用一个新的宏TYPED_TEST,和TEST宏类似

TYPED_TEST_SUITE_P(PrimeTableTest2);
TYPED_TEST_P(PrimeTableTest2, ReturnsFalseForNonPrimes)
REGISTER_TYPED_TEST_SUITE_P(
		PrimeTableTest2,  // 第一个参数是测试用例名称.
		// 其余的参数是测试名称,以下有3个测试名
		ReturnsFalseForNonPrimes, ReturnsTrueForPrimes, CanGetNextPrime);
INSTANTIATE_TYPED_TEST_SUITE_P(OnTheFlyAndPreCalculated,    // 实例名称
		PrimeTableTest2,             // 测试用例名称
		PrimeTableImplementations);  // 类型列表

这里没有整明白,待以后理解,再回来补充。

;