自力更生:0依赖三方库,手把手教你打造专属C++测试框架

前言:

当人们谈到测试框架的时候,首先想到的就是 google 的 gtest, 想着怎么在代码中集成 gtest 的框架,来实现自身代码的测试。 然后就巴拉巴拉的费了老大劲将 gtest 嵌入到自己的代码中来。

诚然,在自身程序接口稳定,且代码量达到一定程度的时候,上 gtest 是一个不错的选择。

但是当自身代码不是很多,并不想带上一个厚重的第三方库的时候,那就得自己想办法搭建测试框架了。

本文中我们来探讨下,在不依赖第三方库的情况下,如何搭建自己的测试框架。

文章目录

    • 前言:
    • 测试框架的组成
    • 一个测试框架的示例
      • 本测试框架的主要组成
      • 各基本组成的主要业务逻辑
        • 测试用例的自动注册
        • 测试用例的执行
        • 测试结果的接收和呈现
    • 总结

测试框架的组成

当我们来设计测试框架的时候,总会想测试框架最基本的组成应该是什么。
我总结三点:

  1. 测试用例的自动注册
  2. 测试用例的执行(自动/半自动/手动)
  3. 测试结果的接收和呈现。

即:测试框架被带起时,要能将待测试项进行自动注册;而当要执行测试用例的时候,又能够手动或自动的执行测试用例;测试用例执行完之后,能将结果汇总并呈现给开发者或用户。

那么我们就通过一个例子,来看下一种高效的解决方案设计是怎么执行这些逻辑的。

一个测试框架的示例

本测试框架的主要组成

class or structdescription
DemoTestStatus一个枚举,定义了测试状态(成功、非致命失败、致命失败)
DemoTestResult一个结构体,用于存储测试结果和相关信息。
DemoTest一个抽象基类,定义了测试用例的基本接口和执行流程。
DemoTestInfo一个类,用于存储测试用例的元数据和结果。
DemoTestFactoryBase一个抽象工厂类,用于创建测试实例。
DemoTestManager一个类,提供测试用例的注册、执行、结果收集和展示等功能。
一系列测试宏用于简化测试用例的创建和注册。

其简化后的结构设计如下:

enum DemoTestStatus
{kSuccess,kNotFatalFailure,kFatalFailure
};struct DemoTestResult
{DemoTestStatus resStatus;std::string resInfo;DemoTestResult() : resStatus(kSuccess), resInfo("Success") {}
};class DemoTest
{
public:virtual ~DemoTest() {}static bool hasFatalFailure();protected:friend class DemoTestinfo;DemoTest();// run the testvoid run();// run the test after the test has been set up.virtual void testing() = 0;// resultDemoTestResult* result() const;void setTestResult(const DemoTestStatus& status, const char* pFmt, ...);
};class DemoTestInfo
{
public:DemoTestInfo(const std::string& name, const std::string& desc, DemoTestFactoryBase* pFactory);~DemoTestInfo();const std::string name() const;const std::string desc() const;void run();DemoTestResult* result() const;private:std::string m_name;std::string m_desc;DemoTestFactoryBase* m_pFactory = nullptr;DemoTestResult m_pResult;
};class DemoTestFactoryBase
{
public:virtual ~DemoTestFactoryBase() {}virtual DemoTest* createTest() = 0;protected:DemoTestFactoryBase() {}DemoTestFactoryBase(const DemoTestFactoryBase&) = delete;DemoTestFactoryBase& operator=(const DemoTestFactoryBase&) = delete;
};template <typename Ts>
class DemoTestFactory : public DemoTestFactoryBase
{
public:virtual DemoTest* createTest() override{return new Ts();}
};extern DemoTestInfo* newAndRegisterTestInfo(const std::string& name, const std::string& desc, DemoTestFactoryBase* pFactory);// test macro#define DEMO_API_TEST_CASE_NAME(test_case_name)    #test_case_name
#define DEMO_API_TEST_CLASS_NAME(test_case_name)   Demo##Test##DEMO_API_TEST_CASE_NAME#define DEMO_API_TEST_FACTORY(test_class_name)  (new DemoTestFactory<test_class_name>())#define DEMO_API_TEST(test_leaf_class, test_base_class) \class DEMO_API_TEST_CLASS_NAME(test_leaf_class) : public test_base_class \{ \public: \DEMO_API_TEST_CLASS_NAME(test_leaf_class)() {} \void testing() override; \private: \static DemoTestInfo* s_pTestInfo; \DEMO_API_TEST_CLASS_NAME(test_leaf_class)(const DEMO_API_TEST_CLASS_NAME(test_leaf_class)&) = delete; \DEMO_API_TEST_CLASS_NAME(test_leaf_class)& operator=(const DEMO_API_TEST_CLASS_NAME(test_leaf_class)&) = delete; \};  \\DemoTestInfo* DEMO_API_TEST_CLASS_NAME(test_leaf_class)::s_pTestInfo = newAndRegisterTestInfo(DEMO_API_TEST_CASE_NAME(test_leaf_class), std::string(), DEMO_API_TEST_FACTORY(DEMO_API_TEST_CLASS_NAME(test_leaf_class))); \\void DEMO_API_TEST_CLASS_NAME(test_leaf_class)::testing()#define DEMO_API_DEFAULT_TEST(test_case_name) DEMO_API_TEST(test_case_name, DemoTest)#define DEMO_API_TEST_RESULT(result) setTestResult(result, "The error occurred in the line:%d, file: %s.\n", __LINE__, __FILE__)class DemoTestManager
{
public:static DemoTestManager* instance();void runTest(const std::string name);void runAllTests();DemoTestInfo* currentTestInfo() const;void setCurrentTestInfo(DemoTestInfo* pInfo);bool hasSameTest() const;void registerTestInfo(DemoTestInfo* pTestInfo);void unregisterTestInfo(const std::string& name);private:DemoTestManager();~DemoTestManager();void showResult(DemoTestInfo* pTestInfo);void showResults(const std::vector<DemoTestInfo*>& testInfos);// SystemStatus checkDefaultLibExistOnce();DemoTestInfo* m_pCurrentTestInfo;std::vector<DemoTestInfo*> m_testInfos;std::vector<DemoTestInfo*> m_smaeNameTestinfos;struct Record{std::vector<DemoTestResult> results;};std::map<DemoTestInfo*, Record> m_records;UiElement* m_pElement = nullptr;
};

当用户写测试用例时,可以这样写:

DEMO_API_DEFAULT_TEST(test_exp_1)
{if (...) {DEMO_API_TEST_RESULT(kNotFatalFailure);return;}...return;
}

各基本组成的主要业务逻辑

测试用例的自动注册

本架构中测试用例的注册是在该模块被加载时,静态被注册到测试框架中的,并以按钮的形式被添加在主程序界面窗口上的。

我们来具体看下业务逻辑:

  1. DEMO_API_DEFAULT_TEST(test_exp_1) 展开,我们看下:
class DemoTesttest_exp_1 : public DemoTest 
{ 
public: DemoTesttest_exp_1() {} void testing() override; private: static DemoTestInfo* s_pTestInfo;DemoTesttest_exp_1(const DemoTesttest_exp_1&) = delete; DemoTesttest_exp_1& operator=(const DemoTesttest_exp_1&) = delete; 
}; DemoTestInfo* DemoTesttest_exp_1::s_pTestInfo = newAndRegisterTestInfo("test_exp_1", std::string(), (new DemoTestFactory<DemoTesttest_exp_1>())); void DemoTesttest_exp_1::testing()
{if (...) {DEMO_API_TEST_RESULT(kNotFatalFailure);return;}...return;
}
  1. 这其中我们看到 DemoTesttest_exp_1 这个子测试用例类有一个静态成员变量:s_pTestInfo,它是在该模块启动阶段就被初始化了。
  2. 我们在来看下它的初始化逻辑。即展开看下 newAndRegisterTestInfo 函数的逻辑。
DemoTestInfo* newAndRegisterTestInfo(const std::string& name, const std::string& desc, DemoTestFactoryBase* pFactory)
{auto pTestInfo = new DemoTestInfo(name, desc, pFactory);DemoTestManager::instance()->registerTestInfo(pTestInfo);return pTestInfo;
}
  1. 在传入参数给这个函数的时候, new 了一个子测试用例类的工厂类,传入函数之后, new 出了与之匹配的自测试用例信息类,然后将其注册给 DemoTestManager.
  2. 我们来继续看下 registerTestInfo() 里面的执行逻辑:
void DemoTestManager::registerTestInfo(DemoTestInfo* pTestInfo)
{if (!pTestInfo) {return;}...UiMenu* pMenu = m_pElement->menu();m_testInfos.push_back(pTestInfo);UiElement* pTestCaseElem = pMenu->addElement(pTestInfo->name());pTestCaseElem->sigTriggered().connect([this](UiElement* pElem) { runTest(pElem->text()); });
}
// 注: 其中以 UiXXX 写法的类均是对 QT API类的二次封装,读者再看时,可以将其替换成QT对应的逻辑即可。
  1. 上面那段业务逻辑,其实核心的就是给该自测试用例关联创建一个可视化的子按钮,点击按钮即可触发该测试用例的逻辑(槽函数机制)。
测试用例的执行
  1. 我们来具体看下,当我们触发了槽函数,又是如何一步一步的执行子测试用例的。
  2. 先看下 runTest() 的具体业务逻辑:
void DemoTestManager::runTest(const std::string name)
{for (auto pTestInfo : m_testInfos) {if (pTestInfo->name() == name) {pTestInfo->run();m_records[pTestInfo].results.push_back(*pTestInfo->result());showResult(pTestInfo);break;}}
}
  1. 其中核心的一条就是调用 DemoTestInforun() 接口:
void DemoTestInfo::run()
{DemoTest* pTest = m_pFactory->createTest();if (!DemoTest::hasFatalFailure()) {pTest->run();}delete pTest;pTest = nullptr;
}
  1. 这里我们可以看到,最终执行的是子测试用例的 run() 接口,即其内部实际上又是调用的子测试用例的testing() 接口,就是本节开头的:
void DemoTesttest_exp_1::testing()
{if (...) {DEMO_API_TEST_RESULT(kNotFatalFailure);return;}...return;
}
  1. 至此,完成该测试项的执行。
测试结果的接收和呈现
  1. 好,完成了测试用例的执行之后,测试用例执行成功与否,失败处的相关信息记录又是怎样的呢?我们继续来看下:
  2. 接着展开子测试用例的测试接口 testing(), 我们会看到承接逻辑判断时,会用一个宏去记录判断的结果,即:DEMO_API_TEST_RESULT
#define DEMO_API_TEST_RESULT(result) \
setTestResult(result, "The error occurred in the line:%d, file: %s.\n", __LINE__, __FILE__)
  1. 将这个函数展开:
void DemoTest::setTestResult(const DemoTestStatus& status, const char* pFmt, ...)
{result()->resStatus = status;std::string info;if (status == kSuccess) {info.append("success");} else {va_list args;va_start(args, pFmt);char buf[BUFFER_LEN] = {};vsnprintf(buf, BUFFER_LEN, pFmt, args);wchar_t wbuf[HALF_BUFFER_LEN] = {};MultiByteToWideChar(CP_UTF8, 0, buf, -1, wbuf, HALF_BUFFER_LEN);info = (std::string)wbuf;va_end(args);}result()->resInfo = info;
}DemoTestResult* DemoTest::result() const
{auto pCurTestInfo = DemoTestManager::instance()->currentTestInfo();if (!pCurTestInfo) {return nullptr;}return pCurTestInfo->result();
}DemoTestResult* DemoTestInfo::result() const
{return const_cast<DemoTestResult*>(&m_pResult);
}
  1. 我们看到它会将结果记录在 DemoTestInfo 下的 m_pResult 对象中。
  2. 当我们执行槽函数之后,触发测试用例的执行,会到这段逻辑中来:
void DemoTestManager::runTest(const std::string name)
{for (auto pTestInfo : m_testInfos) {if (pTestInfo->name() == name) {pTestInfo->run();m_records[pTestInfo].results.push_back(*pTestInfo->result());showResult(pTestInfo);break;}}
}
  1. 其中重要的两点是:
    1) 记录测试信息 m_records[pTestInfo].results.push_back(*pTestInfo->result());

    2) 显示结果:showResult(pTestInfo);

  2. showResult() 的主要业务逻辑是将结果相关的信息回显给开发者或者用户,不同的开发者有不同的回显习惯,在此就不在赘述,由开发者自己去实现。

总结

好了,到现在为止,本文和核心内容阐述就完成了,希望能对想自己动手搭建测试框架的朋友有一点建设性的指导意义。如果能在后续工作或学习中有所帮助,那是再好不过。

下一篇文章,我将带大家一起看下,搭建好的测试框架模块(亦或是独立的其它模块)是如何被主模块给加载起来的,感兴趣的朋友可以看下下期文章。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/19093.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

huggingface的self.state与self.control来源(TrainerState与TrainerControl)

文章目录 前言一、huggingface的trainer的self.state与self.control初始化调用二、TrainerState源码解读(self.state)1、huggingface中self.state初始化参数2、TrainerState类的Demo 三、TrainerControl源码解读(self.control)总结 前言 在 Hugging Face 中&#xff0c;self.s…

C语言实现十进制转任意进制(详解)

主要思路&#xff1a;运用一个数组&#xff0c;通过数字每次取任意进制模&#xff0c;存在数组中&#xff0c; 再通过倒取数组中的数值&#xff0c;来实现进制转换&#xff0c;如果遇到十六进制&#xff0c;利用ASCII码值 数字字符和大写字母 相差55的特性来解决 int main() {…

【芯片验证方法】

术语——中文术语 大陆与台湾的一些术语存在差别&#xff1a; 验证常用的英语术语&#xff1a; 验证&#xff1a;尽量模拟实际应用场景&#xff0c;比对芯片的所需要的目标功能和实现的功能 影响验证的要素&#xff1a;应用场景、目标功能、比对应用场景、目标功能&#xff…

内存泄露和内存溢出有什么不同

内存泄露和内存溢出是两个常见的内存问题&#xff0c;它们在程序运行过程中可能导致性能下降、系统不稳定甚至应用崩溃。虽然这两个术语经常被混用&#xff0c;但它们描述的是两种不同的问题&#xff1a; 内存泄露&#xff08;Memory Leak&#xff09; 内存泄露是指程序在分配…

华发股份:加强业务协同 新政下项目热销

“5.17”楼市政策出台后&#xff0c;各地密集落地执行。5月27—28日&#xff0c;上海、广州、深圳三个一线城市跟进落地“517”新政。上海发布《关于优化本市房地产市场平稳健康发展政策措施的通知》&#xff0c;共计9条调整政策&#xff0c;涵盖外地户籍、人才、单身、婚否、企…

一个生动的例子——通过ERC20接口访问Tether合约

生动的例子 USDT&#xff1a;符合ERC20标准的美元稳定币&#xff0c;Tether合约获得测试网上Tether合约地址通过自己写的ERC20接口访问这个合约 Tether合约地址&#xff1a;0xdAC17F958D2ee523a2206206994597C13D831ec7 IERC20.sol // SPDX-License-Identifier: GPL-3.0pra…

今日分享站

同志们&#xff0c;字符函数和字符串函数已经全部学习完啦&#xff0c;笔记也已经上传完毕&#xff0c;大家可以去看啦。字符函数和字符串函数and模拟函数 加油&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;

Unix环境高级编程--7-进程环境--7.1-7.2main函数-7.3进程退出

1、几个问题 ①main函数如何被调用&#xff1f; ②命令行参数如何传递给新程序&#xff1f;&#xff1b; ③典型储存空间布局是什么样的&#xff1f;&#xff1b; ④进程如何使用环境变量 &#xff1f;&#xff1b; ⑤进程的各种终止方式&#xff1f; 2、main函数 当内核…

列表推导式(解析式)python

Python中的列表推导式&#xff08;list comprehension&#xff09;是一种简洁且强大的语法&#xff0c;用于创建新的列表。它允许你通过对现有列表中的元素进行操作或筛选来快速生成新列表。以下是列表推导式的基本语法和一些示例&#xff1a; 基本语法&#xff1a; new_list…

vue3的组件通信v-model使用

一、组件通信 1.props 》 父向子传值 props 主要用于父组件向子组件通信。再父组件中通过使用:msgmsg绑定需要传给子组件的属性值&#xff0c;然后再在子组件中用props接收该属性值 方法一 普通方式:// 父组件 传值<child :msg1"msg1" :list"list">…

Dinky MySQLCDC 整库同步到 Doris

资源&#xff1a;flink 1.17.0、dinky 1.0.2、doris-2.0.1-rc04 问题&#xff1a;Cannot deserialize value of type int from String &#xff0c;detailMessageunknowndatabases &#xff0c;not a valid int value 2024-05-29 16:52:20.136 ERROR org.apache.doris.flink.…

最长公共子序列问题的求解

假设有两个字符串A和B&#xff0c;A字符串的组成为 A A 0 A 1 A 2 . . . . . . A n − 1 A A_0A_1A_2......A_{n-1} AA0​A1​A2​......An−1​ B B 0 B 1 B 2 . . . . . . B m − 1 BB_0B_1B_2......B_{m-1} BB0​B1​B2​......Bm−1​ 要寻找这两个字符串的公共子序列还…

MS Excel: 高亮当前行列 - 保持原有格式不被改变

本文使用条件格式VBA的方法实现高亮当前行列&#xff0c;因为纯VBA似乎会清除原有的高亮格式。效果如下&#xff1a;本文图省事就使用同一种颜色了。 首先最重要的&#xff0c;【选中你期望高亮的单元格区域】&#xff0c;比如可以全选当前sheet的全部区域 然后点击【开始】-【…

06.深入学习Java 线程

1 线程的状态/生命周期 Java 的 Thread 类对线程状态进行了枚举&#xff1a; public class Thread implements Runnable {public enum State {NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;} } 初始(NEW)&#xff1a;新创建了一个线程对象&#xff0c;但还没有调用…

数据库学习笔记1-数据库实验1

文章目录 创建表格的时候出现的一些错误查询所有的表格实验一查询单个表格分块修改大学数据库表格创建大学数据库表格系课程教师课程段授课学生选课注意吐槽 修改大学数据库表格2&#xff08;英文版本&#xff09;abcde 自建项目-在线书店数据库 创建表格的时候出现的一些错误 …

子集和问题(回溯法)

目录 ​​​​ 前言 一、算法思路 二、分析过程 三、代码实现 伪代码&#xff1a; C&#xff1a; 总结 前言 【问题描述】考虑定义如下的PARTITION问题中的一个变型。给定一个n个整数的集合X{x1,x2,…,xn}和整数y&#xff0c;找出和等于y的X的子集Y。 一、算法思路 基本思想&am…

【STL】C++ stack(栈) 基本使用

目录 一 stack常见构造 1 空容器构造函数&#xff08;默认构造函数&#xff09; 2. 使用指定容器构造 3 拷贝构造函数 二 其他操作 1 empty 2 size 3 top 4 push && pop 5 emplace 6 swap 三 总结 一 stack常见构造 1 空容器构造函数&#xff08;默认构造…

云计算OpenStack基础

1.什么是虚拟化&#xff1f; •虚拟化是云计算的基础。 •虚拟化是指计算元件在虚拟的而不是真实的硬件基础上运行。 •虚拟化将物理资源转变为具有可管理性的逻辑资源&#xff0c;以消除物理结构之间的隔离&#xff0c;将物理资源融为一个整体。虚拟化是一种简化管理和优化…

探秘AI艺术:揭开Midjourney绘画的神秘面纱

在当今这个数字化迅速发展的时代&#xff0c;AI技术已经深入到我们生活的方方面面&#xff0c;而最令人着迷的莫过于它在艺术创作领域的应用。“Midjourney绘画”就是这样一个令人惊叹的例子&#xff0c;它通过高级AI技术&#xff0c;能够帮助用户生成独一无二的艺术作品。但是…

如何知道自己电脑的 Shell类型是什么?

在macOS中&#xff0c;你可以通过以下几种方法来确定当前正在使用的shell类型&#xff0c;并了解相关的配置文件&#xff1a; 1. 使用终端命令确定shell类型 打开终端应用程序&#xff08;Terminal&#xff09;。输入以下命令并按回车键&#xff1a;echo $SHELL。该命令会输出…