当实现一个算法或者写一个工具类的时候,我们总是需要写一些测试代码,但如果全部写在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)