前言
上节我们学习了gtest的各种宏断言
单元测试框架gtest学习(二)—— 认识断言-CSDN博客
本节我们介绍gtets的事件机制
虽然 Google Test 的核心是用来编写单元测试和断言的,但它也允许在测试执行过程中进行事件的钩取和自定义,其主要用于处理测试生命周期中的各种操作,如测试的初始化、清理、事件通知等。
简单来讲,事件机制的作用就是帮我们进行一些测试前的初始化工作和测试后的清理工作
介绍与引出
我们直接上代码进行说明,如下我们实现了一个简单的线程安全队列
// safequeue.h
#ifndef SAFEQUEUE_H
#define SAFEQUEUE_H#include <mutex>
#include <queue>
#include <stdexcept>template <typename E>
class Queue {public:Queue() = default; // 在此定义默认构造函数// 入队操作void Enqueue(const E &element) {std::lock_guard<std::mutex> lock(mtx_);queue_.emplace(element);}// 出队操作E Dequeue() {std::lock_guard<std::mutex> lock(mtx_);if (!queue_.empty()) {E data = queue_.front();queue_.pop();return data;}throw std::runtime_error("Queue is empty");}// 获取队列容量size_t size() const { return queue_.size(); }private:std::queue<E> queue_;std::mutex mtx_;
};#endif // SAFEQUEUE_H
代码写完之后,接下来我们自然想测试一下我们的队列入队操作、出队操作以及队列容量的获取操作这些功能是否符合我们的预期
因此,我们会写出以下测试案例
TEST(QueueTest, DeQueueTest) {Queue<int> queue;queue.Enqueue(1);queue.Enqueue(2);queue.Enqueue(3);for (int i = 1; i <= 3; ++i) {try {int n = queue.Dequeue();// 测试是否出队成功,并且出队的元素符合预期EXPECT_EQ(n, i);} catch (const std::runtime_error &e) {// 如果抛出异常,判断抛出异常的原因是否是因为队列为空EXPECT_STREQ(e.what(), "Queue is empty");}}
}TEST(QueueTest, QueueSize) {Queue<int> queue;queue.Enqueue(1);queue.Enqueue(2);queue.Enqueue(3);EXPECT_EQ(queue.size(), 3);
}
编译运行,查看结果
从结果上看,
- 我们进行了两个测试案例
- 一个用于测试队列的出队操作
- 另一个用于测试队列的容量是否符合预期
- 这两个测试案例的被测对象都同属于一个,即QueueTest
由此,我们引出两个概念
- test suite:直译为测试套件,在上述代码中指QueueTest
- test case:直译为测试案例,上述代码中指DeQueueTest和QueueSize
因此,直观上看, test suite可以理解为我们被测的对象是谁,而 test case可以理解我们要测试的功能是哪些,其中这些功能是属于上述被测对象的
关于测试套件和测试案例的概念点到为止,接下来我们将目光继续放在代码上
观察上述两个功能的测试
- 我们在对每个功能进行测试前,都对队列进行了入队操作
- 这个入队操作从另一个角度可以理解为测试前的初始化操作(毕竟只有队列中有数据才能进行出队和容量测试)
显而易见的,当我们的测试功能越来越多时,这种类同的初始化工作每次都要写一遍显然是在浪费时间和效率,同样的道理,在测试结束之后我们也是需要进行清理工作的,只是恰好我们这里的清理工作每次都由析构函数做了
因此,对于一些测试而言,我们需要在每个测试的前后进行一些初始化操作和清理工作,那么问题来了,有没有一种办法能够一次性将这些初始化和清理工作完成,而不必我们每次都要在写测试案例的手动进行处理呢?
这就引出了我们今天要说的事件机制,直观上看,事件机制的作用好像是构造函数和析构函数的作用
事件机制分类
事件机制分为三类:
1. 全局的,也就是在所有案例执行前进行初始化操作,在所有案例结束后进行清理操作。
2. TestSuite级别的,在某一批案例中第一个案例前进行初始化操作,最后一个案例执行后进行清理操作。
3. TestCase级别的,每个TestCase前后都进行一次初始化和清理。
全局事件
全局事件主要用于对全局环境的初始化和清理
要实现全局事件,必须写一个类,继承testing::Environment类,实现里面的SetUp和TearDown方法,其中
1. SetUp()方法在所有案例执行前执行
2. TearDown()方法在所有案例执行后执行
#if 1
class MyTestEnvironment : public testing::Environment {public:// 在所有测试开始之前调用void SetUp() override {std::cout << "Global SetUp: 初始化全局事件\n";// 在这里进行全局初始化,比如资源分配}// 在所有测试完成之后调用void TearDown() override {std::cout << "Global TearDown: 清理全局事件\n";// 在这里进行全局清理,比如释放资源}
};// 测试夹具类
class QueueTest : public ::testing::Test {protected:void SetUp() override {// 在每个测试用例前执行std::cout << "Test SetUp: 准备测试环境\n";}void TearDown() override {// 在每个测试用例后执行std::cout << "Test TearDown: 清理测试环境\n";}
};// 定义你的测试用例
TEST_F(QueueTest, IsEmptyInitially) {std::cout << "执行 IsEmptyInitially 测试\n";// 这里可以写你的测试逻辑
}TEST_F(QueueTest, DequeueWorks) {std::cout << "执行 DequeueWorks 测试\n";// 这里可以写你的测试逻辑
}
#endif
我们还需要告诉gtest添加这个全局事件,我们需要在main函数中通过testing::AddGlobalTestEnvironment方法将事件挂进来,也就是说,我们可以写很多个这样的类,然后将他们的事件都挂上去。
int main(int argc, char **argv) {// 注册我们自定义的全局环境testing::AddGlobalTestEnvironment(new MyTestEnvironment);testing::InitGoogleTest(&argc, argv);return RUN_ALL_TESTS();
}
另外,我们观察上述代码,我们的断言宏已经不再是TEST,而是TEST_F了,这一点需要注意,如果我们使用事件机制进行测试,则需要使用TEST_F,而不能使用TEST宏
编译运行
TestSuite事件
同理TestSuite事件也需要我们写一个类,继承testing::Test,然后实现两个静态方法
1. SetUpTestSuite() 方法在第一个TestCase之前执行
2. TearDownTestSuite() 方法在最后一个TestCase之后执行
#if 1
class QueueTest : public testing::Test {protected:// 这是每个测试用例的初始化void SetUp() override {std::cout << " 初始化每个TestCase环境...." << std::endl;q1_.Enqueue(1);q2_.Enqueue(2);q2_.Enqueue(3);}// 这是每个测试用例的清理void TearDown() override {std::cout << " 清理每个TestCase环境...." << std::endl;}// 用于测试的队列对象Queue<int> q0_;Queue<int> q1_;Queue<int> q2_;public:// 静态方法,用于测试套件级别的初始化static void SetUpTestSuite() {std::cout << "初始化整个TestSuite环境...." << std::endl;}// 静态方法,用于测试套件级别的清理static void TearDownTestSuite() {std::cout << "清理整个TestSuite环境...." << std::endl;}
};TEST_F(QueueTest, IsEmptyInitially) { EXPECT_EQ(q0_.size(), 0); }TEST_F(QueueTest, DequeueWorks) {try {int n = q0_.Dequeue();FAIL() << "Expected exception for empty queue";} catch (const std::runtime_error &e) {EXPECT_STREQ(e.what(), "Queue is empty");}int n = q1_.Dequeue();EXPECT_EQ(n, 1);EXPECT_EQ(q1_.size(), 0);n = q2_.Dequeue();EXPECT_EQ(n, 2);EXPECT_EQ(q2_.size(), 1);n = q2_.Dequeue();EXPECT_EQ(n, 3);EXPECT_EQ(q2_.size(), 0);
}
#endif
编译运行
TestCase事件
TestCase事件是挂在每个案例执行前后的,实现方式和上面的几乎一样,不过需要实现的是SetUp方法和TearDown方法:
1. SetUp()方法在每个TestCase之前执行
2. TearDown()方法在每个TestCase之后执行
仍旧使用上述案例代码,观察运行结果发现
实战
接下来我们写一个实际案例来运用上述事件机制
首先我们实现了一个简单的线程池,然后我们来测试这个线程池的运行是否可以正常运行(注意这里是测试功能是否符合预期,而不是进行压测)
//threadPool.h
#ifndef THREADPOOL_H
#define THREADPOOL_H#include <atomic>
#include <condition_variable>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>class ThreadPool {public:ThreadPool(int size = std::thread::hardware_concurrency()): pool_size_(size), isStop_(false) {for (int i = 0; i < pool_size_; ++i) {// threads_.push_back(std::thread(&ThreadPool::worker,this));threads_.emplace_back([this]() { worker(); });}}~ThreadPool() { ShutDown(); }void ShutDown() {isStop_ = true;not_empty_cond_.notify_all();for (auto &thread : threads_) {if (thread.joinable())thread.join();}}template <typename F, typename... Args>auto Submit(F &&f, Args &&...args) -> std::future<decltype(f(args...))> {using func_type = decltype(f(args...));auto task_ptr = std::make_shared<std::packaged_task<func_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));std::future<func_type> func_future = task_ptr->get_future();{std::lock_guard<std::mutex> lock(mtx_);if (isStop_)throw std::runtime_error("threadpool has stop!!!");task_queue_.emplace([task_ptr]() { (*task_ptr)(); });}not_empty_cond_.notify_one();return func_future;}private:std::atomic_bool isStop_;int pool_size_;using Task = std::function<void()>;std::vector<std::thread> threads_;std::queue<Task> task_queue_;std::mutex mtx_;std::condition_variable not_empty_cond_;void worker() {while (1) {std::unique_lock<std::mutex> lock(mtx_);not_empty_cond_.wait(lock, [this]() { return isStop_ || !task_queue_.empty(); });if (isStop_ && task_queue_.empty())return;Task task = task_queue_.front();task_queue_.pop();lock.unlock();task();}return;}
};#endif // THREADPOOL_H
接下来我们写了两个函数来测试线程池是否运行正常
int add(const int &num1, const int &num2) { return num1 + num2; }
如果不使用gtest,我们对上述函数的测试代码是这样的
void addTest() {ThreadPool pool;auto task1 = pool.Submit(add, 1, 2);auto task2 = pool.Submit(add, 3, 4);int sum = task1.get() + task2.get();std::cout << "sum=" << sum << std::endl;
}
显然,如果上述打印输出的结果sum等于10,说明符合预期,测试通过
// 矩阵乘法
std::vector<std::vector<int>>
MatrixMultiply(const std::vector<std::vector<int>> &A,const std::vector<std::vector<int>> &B, ThreadPool &pool) {size_t m = A.size(); // A的行数size_t n = A[0].size(); // A的列数size_t p = B[0].size(); // B的列数std::vector<std::vector<int>> C(m, std::vector<int>(p, 0)); // 结果矩阵std::vector<std::future<void>> futures;// 为结果矩阵中的每个元素提交一个计算任务for (size_t i = 0; i < m; ++i) {for (size_t j = 0; j < p; ++j) {futures.push_back(pool.Submit([i, j, &A, &B, &C, n]() {int sum = 0;for (size_t k = 0; k < n; ++k) {sum += A[i][k] * B[k][j];}C[i][j] = sum;}));}}// 等待所有的任务完成for (auto &future : futures) {future.get();}return C;
}
同理,对于上述矩阵乘法,如果我们不使用gtest来测试,代码应该是这样的
std::ostream &operator<<(std::ostream &os,std::vector<std::vector<int>> &nums) {int col = 0;int m = nums.size();int n = nums[0].size();for (int i = 0; i < m; ++i) {for (int j = 0; j < n; ++j) {os << nums[i][j] << " ";col++;if (col % n == 0)os << "\n";}}return os;
}void multiTest() {// 定义两个矩阵 A 和 Bstd::vector<std::vector<int>> A = {{1, 2, 3}, {4, 5, 6}};std::vector<std::vector<int>> B = {{7, 8}, {9, 10}, {11, 12}};ThreadPool pool(A.size() * B[0].size());// 计算矩阵 A 和 B 的乘积std::vector<std::vector<int>> C =MatrixMultiply(A, B, pool); // 使用 *pool_ 来解引用智能指针// std::vector<std::vector<int>> expected = {{58, 64}, {139, 154}};std::cout << C;
}
其实就是很简单的,把函数运行一下,然后打印输出结果,观察运行结果是否符合预期
只是在gtest中使用各种断言宏来帮助我们判断,而不是打印出来然后肉眼我们判断
好,接下来我们使用gtest来进行测试
既然我们要进行两个功能的测试(加法功能和矩阵乘法功能),每个功能在在测试之前都需要对线程池进行初始化,那么我们使用上述事件机制,进行一些初始化工作
class ThreadPoolTest : public ::testing::Test {public:void SetUp() override {std::cout << ">>>> init threadpool...." << std::endl;pool_ = std::make_unique<ThreadPool>(4); // 假设线程池初始化为4个线程}void TearDown() override {std::cout << ">>>> shutdown threadPool...." << std::endl;pool_->ShutDown();}std::unique_ptr<ThreadPool> pool_;
};
这样我们就不用在每个测试案例进行初始化工作了,只需要关注测试代码的逻辑编写即可
以下是加法功能的测试代码
TEST_F(ThreadPoolTest, TestThreadPoolSubmit) {// 提交加法任务auto task1 = pool_->Submit(add, 1, 2); // 1 + 2auto task2 = pool_->Submit(add, 3, 4); // 3 + 4// 获取任务的结果int sum = task1.get() + task2.get(); // 获取两个任务的结果// 验证最终结果EXPECT_EQ(sum, 10);
}
以下是矩阵乘法的测试代码
TEST_F(ThreadPoolTest, TestMatrixMultiplication) {// 定义两个矩阵 A 和 Bstd::vector<std::vector<int>> A = {{1, 2, 3}, {4, 5, 6}};std::vector<std::vector<int>> B = {{7, 8}, {9, 10}, {11, 12}};// 计算矩阵 A 和 B 的乘积std::vector<std::vector<int>> C =MatrixMultiply(A, B, *pool_); // 使用 *pool_ 来解引用智能指针// 验证结果矩阵 Cstd::vector<std::vector<int>> expected = {{58, 64}, {139, 154}};EXPECT_EQ(C, expected);
}
完整代码如下
#include "threadPool.h"
#include <gtest/gtest.h>#if 1
class ThreadPoolTest : public ::testing::Test {public:void SetUp() override {std::cout << ">>>> init threadpool...." << std::endl;pool_ = std::make_unique<ThreadPool>(4); // 假设线程池初始化为4个线程}void TearDown() override {std::cout << ">>>> shutdown threadPool...." << std::endl;pool_->ShutDown();}std::unique_ptr<ThreadPool> pool_;
};// 矩阵乘法
std::vector<std::vector<int>>
MatrixMultiply(const std::vector<std::vector<int>> &A,const std::vector<std::vector<int>> &B, ThreadPool &pool) {size_t m = A.size(); // A的行数size_t n = A[0].size(); // A的列数size_t p = B[0].size(); // B的列数std::vector<std::vector<int>> C(m, std::vector<int>(p, 0)); // 结果矩阵std::vector<std::future<void>> futures;// 为结果矩阵中的每个元素提交一个计算任务for (size_t i = 0; i < m; ++i) {for (size_t j = 0; j < p; ++j) {futures.push_back(pool.Submit([i, j, &A, &B, &C, n]() {int sum = 0;for (size_t k = 0; k < n; ++k) {sum += A[i][k] * B[k][j];}C[i][j] = sum;}));}}// 等待所有的任务完成for (auto &future : futures) {future.get();}return C;
}
// 验证矩阵乘法结果是否正确
TEST_F(ThreadPoolTest, TestMatrixMultiplication) {// 定义两个矩阵 A 和 Bstd::vector<std::vector<int>> A = {{1, 2, 3}, {4, 5, 6}};std::vector<std::vector<int>> B = {{7, 8}, {9, 10}, {11, 12}};// 计算矩阵 A 和 B 的乘积std::vector<std::vector<int>> C =MatrixMultiply(A, B, *pool_); // 使用 *pool_ 来解引用智能指针// 验证结果矩阵 Cstd::vector<std::vector<int>> expected = {{58, 64}, {139, 154}};EXPECT_EQ(C, expected);
}int add(const int &num1, const int &num2) { return num1 + num2; }
TEST_F(ThreadPoolTest, TestThreadPoolSubmit) {// 提交加法任务auto task1 = pool_->Submit(add, 1, 2); // 1 + 2auto task2 = pool_->Submit(add, 3, 4); // 3 + 4// 获取任务的结果int sum = task1.get() + task2.get(); // 获取两个任务的结果// 验证最终结果EXPECT_EQ(sum, 10);
}
#endif#if 1
int main(int argc, char **argv) {// testing::AddGlobalTestEnvironment(new FooEnvironment);testing::InitGoogleTest(&argc, argv);return RUN_ALL_TESTS();
}
#endif
运行结果如下所示
[ 11:08上午 ] [ pcl@robot:~/projects/myPro/threadPool/test(main✗) ]$ g++ ./gTest.cc -o gtest -std=c++14 -lgtest -lpthread
[ 11:08上午 ] [ pcl@robot:~/projects/myPro/threadPool/test(main✗) ]$ ./gtest
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from ThreadPoolTest
[ RUN ] ThreadPoolTest.TestMatrixMultiplication
>>>> init threadpool....
>>>> shutdown threadPool....
[ OK ] ThreadPoolTest.TestMatrixMultiplication (3 ms)
[ RUN ] ThreadPoolTest.TestThreadPoolSubmit
>>>> init threadpool....
>>>> shutdown threadPool....
[ OK ] ThreadPoolTest.TestThreadPoolSubmit (1 ms)
[----------] 2 tests from ThreadPoolTest (4 ms total)[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (5 ms total)
[ PASSED ] 2 tests.