一个不足百行的单元测试框架: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失败.
下面就是实现,你也可以下载该文件:http://code.google.com/p/baiyanhuang/source/browse/trunk/LazyLib/LazyTest.h
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)

 

 

posted @   lzprgmr  阅读(2837)  评论(2编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述

黄将军

点击右上角即可分享
微信分享提示