一个不足百行的单元测试框架:LazyTest
当实现一个算法或者写一个工具类的时候,我们总是需要写一些测试代码,但如果全部写在main函数里,难免组织混乱,不易清查;如果选用cppunit或者gtest等强大的单元测试框架,又是杀鸡用牛刀 - 太重了,不方便。另外一个可选的是TUT, Template Unit Test Framework,与前两者不同是其采用C++模板函数实现,而不是宏,虽说号称短小精悍,拿来一试也觉得颇显富态。
其实我只需要一个很简单的框架,只是针对一个算法实现,或者一个工具类写测试,而不是项目级别的。比如我写了一个max函数求两个数中较大的那个,那么测试代码可以这么写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | TESTCASE(test_max_int) { ASSERT_TRUE(max(1, 10) == 10); ASSERT_TRUE(max(100, 10) == 100); ASSERT_TRUE(max(10, 10) == 10); return true ; } TESTCASE(test_max_float) { ASSERT_TRUE(max(1.1, 10.1) == 10.1); ASSERT_TRUE(max(100.1, 10.1) == 100.1); ASSERT_TRUE(max(10.1, 10.1) == 10.1); return true ; } |
然后RUN_ALL_CASES就可以了。
仔细想了一下,这个也不难实现,主要考虑这么几个方面:
- test case的自动注册
这个可以在声明TESTCASE时用一个全局静态变量的构造函数实现 - test case的管理与运行
只要将所有的case注册到一个容器中,最后遍历该容器调用case即可 - 宣告case失败并提高错误信息
用一个宏来检查某个表达式,若失败则做两件事:一是output错误行与表达式;二是返回false宣告case失败.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | // // Description: // A simple unit-test framework which aims to testing simple programs like utility class, algorithm... // // How to use: // You only need to know 3 macros to use this framework: TESTCASE, ASSERT_TRUE, RUN_ALL_CASES // TESTCASE(testname) // { // ASSERT_TRUE(1 + 1 == 2); // return true; // } // ... // RUN_ALL_CASES(); // // Author: lzprgmr // Date: 1/8/2011 // #pragma once #include <map> #include <iostream> #if defined(_WIN32) #include <Windows.h> #endif #if !defined(LazyTestOut) #define LazyTestOut std::cout #endif // typedefs typedef unsigned int uint32_t; typedef bool (*TestFunc) (); typedef std::map< char *, TestFunc> TestCaseMap; // Manage and run all test cases class TestMgr { public : static TestMgr* Get() { static TestMgr _instance; return &_instance; } void AddTest( char * tcName, TestFunc tcFunc) { m_tcList[tcName] = tcFunc; } uint32_t RunAllCases() { uint32_t failure = 0; for (TestCaseMap::iterator it = m_tcList.begin(); it != m_tcList.end(); ++it) { LazyTestOut << "Running " << it->first << "... " << std::endl; bool bRes = RunCase(it->second); if (bRes) LazyTestOut << "\tPass" << std::endl; else failure++; } LazyTestOut << "\n" << "Totally " << failure << " cases failed!!!" << std::endl; return failure; } private : bool RunCase(TestFunc tf) { bool bRes = false ; #if defined(_WIN32) // Windows use SEH to handle machine exceptions __try { bRes = tf(); } __except(EXCEPTION_EXECUTE_HANDLER) { LazyTestOut << "\tException caught!" << std::endl; bRes = false ; } #else //Non-Windows OS that doesn't support SEH - the singal mechanism (SIGSEGV) can't work well as SEH to handle the problem bRes = tf(); #endif return bRes; } private : TestCaseMap m_tcList; }; // Register a test case class TestCaseRegister { public : TestCaseRegister( char * tcName, TestFunc tcFunc) { TestMgr::Get()->AddTest(tcName, tcFunc); } }; // To use this test framework, you only need to know 3 macros: #define TESTCASE(tc) \ bool tc(); \ TestCaseRegister register_##tc(#tc, tc); \ bool tc() #define ASSERT_TRUE(expr) do {if(!(expr)) { \ LazyTestOut << "\tFailed at: " << __FILE__ << ": Line " <<__LINE__ << std::endl; \ LazyTestOut << "\tExpression: " << #expr << std::endl; \ return false ;}} while ( false ) #define RUN_ALL_CASES() do {TestMgr::Get()->RunAllCases(); } while(false) |
如果我运行以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #include "../LazyLib/LazyTest.h" TESTCASE(test1) { ASSERT_TRUE(1 + 1 == 2); } TESTCASE(test2) { ASSERT_TRUE(1 + 1 != 2); } TESTCASE(test3) { #if defined(_WIN32) int * p = NULL; *p = 10; #endif ASSERT_TRUE(1 + 1 > 2); } int main() { RUN_ALL_CASES(); return 0; } |
输出结果如下:
Running test1... Pass Running test2... Failed at: c:\source\baiyanhuang\algorithm\test.cpp: Line 12 Expression: 1 + 1 != 2 Running test3... Exception caught! Totally 2 cases failed!!!
这里需要注意的几点是:
- 该代码可以在mac和windows下运行,linux下没试过,应该也可以。但是只有在Windows下用SEH对内存访问错误等硬件错误进行了处理,Mac下singal机制对SIGSEGV的处理不能像SEH那样很好的解决这个问题。
- 写case的时候,case名字不能重复(废话?),并且必须在每个case最后返回true - 这个可能可以简化一下,还没想到怎么做~~~
- 信息默认输出到std::out,你也可以在include该文件之前先define自己的LazyTestOut
更新:
对于每个case必须在最后显示的返回true的问题,这里可以用一个静态类来解决,主要是用一个静态成员保持状态,并由一个有返回值的函数转调我们编写的case,需要修改两个宏定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // To use this test framework, you only need to know 3 macros: #define TESTCASE(tc) \ class class_##tc \ { \ public : \ static bool tc() \ { \ _result = true ; \ run(); \ return _result; \ } \ static void run(); \ private : \ static bool _result; \ }; \ bool class_##tc::_result = true ; \ TestCaseRegister register_##tc(#tc, class_##tc::tc); \ void class_##tc::run() #define ASSERT_TRUE(expr) do {if(!(expr)) { \ LazyTestOut << "\tFailed at: " << __FILE__ << ": Line " <<__LINE__ << std::endl; \ LazyTestOut << "\tExpression: " << #expr << std::endl; \ _result = false ; return ;}} while ( false ) |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述