一个不足百行的单元测试框架:LazyTest
当实现一个算法或者写一个工具类的时候,我们总是需要写一些测试代码,但如果全部写在main函数里,难免组织混乱,不易清查;如果选用cppunit或者gtest等强大的单元测试框架,又是杀鸡用牛刀 - 太重了,不方便。另外一个可选的是TUT, Template Unit Test Framework,与前两者不同是其采用C++模板函数实现,而不是宏,虽说号称短小精悍,拿来一试也觉得颇显富态。
其实我只需要一个很简单的框架,只是针对一个算法实现,或者一个工具类写测试,而不是项目级别的。比如我写了一个max函数求两个数中较大的那个,那么测试代码可以这么写:
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失败.
// // 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)
如果我运行以下代码:
#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,需要修改两个宏定义:
// 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)