[讨论] 测试驱动开发:XP,敏捷方法的基础

辛昕   2012-9-12 23:09 楼主
不多说太多,测试,尤其是自动测试,对于高效创建软件,意义是非常重大的。

如何构筑测试?

java有junit,C也不落后,C下有Unity,C++下有CppTest。
C下其实还有CUTest,CTest......
而在这里,我把宝全押在 Unity下,因为我手头唯一能找到的比较全面的学习资料,就是它。
一本书 测试驱动开发的嵌入式C语言开发。

另一方面,Unity是专门为 嵌入式领域C语言而生的。

好了,就是说这么多。
下面开始,我将依次分享我 学习 这个 框架的过程。

今晚的内容是 首先理清了它 安装测试用例 和 背后的一系列调用过程的实现。

这个帖子的附件,还将附上我今晚用的源码和文件夹,它最初从 书的配套网站上下来的。
但是因为我不是在linux下做,而是在windows下MinGW下弄的,所以,makefile用不上,我直接把Unity里的源码和
测试源码都先扔到一个文件夹里去。

[ 本帖最后由 辛昕 于 2012-9-12 23:11 编辑 ]

    My Unity.rar (2012-9-12 23:11 上传)

    32.78 KB, 下载次数: 42

强者为尊,弱者,死无葬身之地

回复评论 (9)

  1. /*
  2.     这是Unity的主函数;
  3.         显然
  4.         1.UnityGetCommandLineOptions() 是读取命令行的,但是目前不知道怎么用是真的;
  5.         2.UnityFixture.RepeatCount;
  6.           这应该是一个内部静态结构体;
  7.           它绝大多数是在 上述的 UnityGetCommandLineOptions()中完成设置;
  8.         3.这个announceTestRun()倒没什么特别,就是一个程序开始的输出;
  9.         4.UnityBegin()   里头就这 Unity.NumberOfTests = 0;
  10.         5.这是真正执行的测试函数;
  11.         6.剩下这两个都没啥特别;
  12.           一个换行——这个我喜欢;
  13.           UnityEnd();
  14.              那就是输出结果;
  15. */
  16. int UnityMain(int argc, char* argv[], void (*runAllTests)())
  17. {
  18.     int result = UnityGetCommandLineOptions(argc, argv);
  19.     int r;
  20.     if (result != 0)
  21.         return result;

  22.     for (r = 0; r < UnityFixture.RepeatCount; r++)
  23.     {
  24.         announceTestRun(r);
  25.         UnityBegin();
  26.         runAllTests();
  27.         UNITY_OUTPUT_CHAR('\n');
  28.         UnityEnd();
  29.     }

  30.     return UnityFailureCount();
  31. }

  32. /*
  33.     现在麻烦的地方在于:
  34.         RUN_TEST_GROUP(group);
  35.         这个宏往下走,但是我看的不是太懂;
  36.         //Call this from main
  37.     #define RUN_TEST_GROUP(group)\
  38.     void TEST_##group##_GROUP_RUNNER();\
  39.     TEST_##group##_GROUP_RUNNER();
  40.        
  41.         如果是这样一来,比如说:
  42.         RUN_TEST_GROUP(sprintf) 就相当于:
  43.         天!刚懂,它居然给我自动生成一个函数了!!
  44.         并且执行了。
  45.         void TEST_sprintf_GROUP_RUNNER();
  46.         TEST_sprintf_GROUP_RUNNER();
  47.        
  48.         也就是说,这个宏,就是生成一个函数。
  49.         但是它只有一个函数声明 和 直接调用;
  50. */
  51. static void RunAllTests(void)
  52. {
  53.     RUN_TEST_GROUP(sprintf);
  54. }

  55. /*
  56. 现在来注意一个宏,我一直没留意
  57. //This goes at the bottom of each test file or in a separate c file
  58. #define TEST_GROUP_RUNNER(group)\
  59.     void TEST_##group##_GROUP_RUNNER_runAll();\
  60.     void TEST_##group##_GROUP_RUNNER()\
  61.     {\
  62.         TEST_##group##_GROUP_RUNNER_runAll();\
  63.     }\
  64.     void TEST_##group##_GROUP_RUNNER_runAll()
  65.        
  66.         TEST_GROUP_RUNNER(sprintf)
  67.         相当于
  68.         void TEST_sprintf_GROUP_RUNNER_runAll();
  69.        
  70.         void TEST_sprintf_GROUP_RUNNER()
  71.         {
  72.              TEST_sprintf_GROUP_RUNNER_runAll();
  73.         }
  74.         void TEST_sprintf_GROUP_RUNNER_runAll()
  75.        
  76.         这等于说,通过这么一个宏,一下晃悠,就把这个函数给改名了。
  77.         我唯一有点不解
  78.         为什么要绕这么个弯子?
  79.         哦,必须绕,为啥?
  80.         你不能在定义那个宏()
  81.         的时候就这么给写了它的实现啊!!
  82.         Jesus!!
  83.         它居然办到了!!
  84. */
  85. TEST_GROUP_RUNNER(sprintf)
  86. {
  87. ..............
  88. ..............
  89. }

  90. /*
  91.    现在我们来梳理一下
  92.    首先,TEST_GROUP_RUNNER(group)
  93.    这的确就是一个安装测试用例的宏
  94.    经它包装,等同于 把 它下面包含的语句实际上安装到了
  95.    TEST_##group##_GROUP_RUNNER_runAll()这个函数的实现里去了。
  96.    
  97.    而这个
  98.    TEST_##group##_GROUP_RUNNER_runAll()
  99.    则这个
  100.    TEST_##group##_GROUP_RUNNER_runAll()
  101.    则实际通过
  102.    TEST_##group##_GROUP_RUNNER()
  103.    来调用;
  104.    
  105.    最后一次,这个
  106.    TEST_##group##_GROUP_RUNNER()则是由一个宏
  107.    RUN_TEST_GROUP(group)来调用;
  108.    
  109.    我们由此理清了这层复杂的调用关系;
  110.    我相信,现在你和我一样,都对一个问题非常感兴趣。
  111.    通过 ## 这个 连接字符串 的宏,我很早就知道这个宏的功能,当然,第一次见识到它大显神威那莫过于这次了。
  112.    但是,这个调用关系是不是有些太复杂了些?为什么非的这样干?
  113.    
  114.    既然我觉得它复杂,那我就试试能否简单完成?
  115.    最关键之处无非在于
  116.    通过 一个 宏 把 一组用户定义的测试用例 安装到 UnityMain()里;
  117.    
  118.    我想了一阵子,一时半会没能理解这么做的目的何在。
  119.    于是乎,连目的都没想到,那我怎么可能会想到替代的方案呢?
  120.    所以,呵呵,欲知后事,只能留待明晚分解了........
  121. */
强者为尊,弱者,死无葬身之地
点赞  2012-9-12 23:15
专业的东西,观望,顶起~~~~~~~~~~~
点赞  2012-9-13 19:50
基本上,已经把这个例子的例子看懂了。 但是这样写有点乱,所以,我先贴一个大体的框架。
  1. /*
  2. 最简单的总结:
  3. Alltest.c
  4. 1 : RUN_TEST_GROUP() 调用一整组的测试用例,而它们在 测试组容器 TEST_GROUP_RUNNER()中完成间接调用;
  5. SprintTestRunner.c
  6. 2: TEST_GROUP_RUNNER() 间接调用 一系列 RUN_TEST_CASE;
  7. SprintTest.c
  8. 3: 而它们调用的函数 实际上由 TEST()生成
  9. 头两层 通过 组名,比如 sprintf来通信;
  10. 而2 3 组,TEST_GROUP_RUNNER不直接和TEST通信,而是 由RUN_TEST_CASE:它们通过两个参数通信 sprintf 和 具体的测试动作 比如 InsertString;
  11. */
  12. /*
  13. 如果要同时测试多个组,则在 main中调用 多个RUN_TEST_GROUP()即可;
  14. */
[ 本帖最后由 辛昕 于 2012-9-14 14:16 编辑 ]
强者为尊,弱者,死无葬身之地
点赞  2012-9-14 14:14
沙发贴里已经说明白了 从 RUN_TEST_GROUP()到 TEST_GROUP_RUNNER()的过程;
下面简单提一笔,再往下走:

从主调函数开始
main()里
通过 UnityMain()   调用了 RUN_TEST_GROUP(sprintf)里的东西;


RUN_TEST_GROUP(sprintf)  这个宏通过两次变换,实际上是 调用了 由 TEST_GROUP_RUNNER()这个宏间接调用的 一组函数;

//========================================================

/*
TEST_GROUP_RUNNER()里是什么?

是一系列
RUN_TEST_CASE()这个宏展开来,实际上是 声明这个函数 和 调用它的语句;
它调用的这个函数 其实名字是

TEST_group_name_run();
*/

/*
而  这个函数的真正生成之处
是 TEST()这个宏;

TEST()这个宏 展开
比较复杂,
它首先声明了一个 TEST_...._testBody();

然后经由 TEST_group_name_run()来调用Unity自身的 API
UnityTestRunner()调用回这个 TEST_...._testBody();
---- 所以说,        TEST其实只是生成 测试用例,,而调用 则在 RUN_TEST_CASE中完成;
//------------------------------------------------------------------

最后,再写这个 TEST_..._testBody()的函数头,由此,

TEST(){}里的语句,就被安装到了TEST_..._testBody()这个函数里去了。

所以简单的说
TEST()是实际写测试用例的地方,经过上述的过程,它最终被 UnityTestRunner()调用,完成测试过程;

当然那,这个函数还需要其他指针,这些是 这个TEST()函数所需的 设置,清理函数,所以。
如同这个样例,一个很不错的组织的方式就是

TEST_SETUP等宏 和 这个TEST()放在一个源文件,它们组成了 测试用例的安装 和 初始化 清理。
*/

/*
这个过程下来,我们现在已经理解了 全部的调用过程,接下去我们要做的是,针对我们要做的测试,使用Unity本身的API 和宏 完成,
就像 TEST()中写的 TEST_ASSERT_EQUAL()这些 断言 所做的那样。

不过,我始终对一个问题充满疑惑:
这些过程中有一个方法从头贯到脚:

通过一个宏 声明 和 调用 一个函数,再通过另一个宏 去生成另一个调用这个函数 的 函数,然后再用这个函数取代宏的名字,完成函数的编写。

这种方式,精彩之极,但是否复杂了一些?我想仔细考虑一下它的目的和必要性。
*/
强者为尊,弱者,死无葬身之地
点赞  2012-9-14 14:20
首先是目的

很显然,它这样操作是 实现了 函数生成 和 函数调用 的分离;

中译本 把 TEST()这部分叫做 测试用例;
把 TEST_SETUP TEST_TEAR_DOWN 称为测试夹具,并解释说,
“使用测试夹具的目的是避免重复,把所有测试都需要的那些部分组织在一起”

这句话没看懂,看源码,实际上我们已经知道了,,TEST_SETUP 和 TEST_TEAR_DOWN实际上是

初始化 和 清理 函数。

从这里来看,它应该是指 同一个测试对象(sprintf)的测试函数 TEST(),它们通过第二个元来生成不同测试对象,简而言之,
不同的sprintf测试函数;
它是指 对这一系列测试函数的一次性 初始化 和 清理吧。


然后 TEST_GROUP_RUNNER被称为 测试容器,
而很显然,RUN_TEST_GROUP自然就是指 调用测试容器中的测试用例。

其实这个测试容器,翻译成 测试组容器 也许和上下文会更好理解,为何呢?

实际上是,从宏的参数都可以看出来。

RUN_TEST_GROUP 这是调用者
TEST_GROUP_RUNNER这是生成者;

它们针对的对象是 sprintf,也就是要测试的东西的 这一整个测试组。

而后,在 TEST()这个 测试用例中,依然可以从宏的参数看出来。
它不仅带了测试对象,还带了具体的测试内容;
比如TEST(sprintf,InsertString),sprintf是被测试对象,对应上面的组名
InsertString是指这个测试用例本身测试的是 插入字符串这个测试内容;

所以,最后,TEST()生成的这些 同一个测试对象的具体不同的 测试内容 将被 TEST_GROUP_RUNNER这个测试组的容器所 包含,调用。

说完目的,我们似乎也已经理解了 必要性,而它也是 ## 这个字符串连接预处理功能 大显神威 的关键所在。

第一,你事先不知道你到底要生成多少个 函数,包括 测试用例,以及 中间的调用层,你都不知道。
因此,你无法预先声明,更无法自动调用;

第二,调用 和 生成 要分离,也不得不分离。
而 生成函数以后,函数的实现,具体的代码要可以由你通过宏代入,这是 中间调用层 存在的理由。
比如 上述的 TEST_group_name_run(),它存在的意义实际上只是 调用 TEST_..._testBody();
但是,如果你不经由这一层,那么,你就没办法 分离 RUN_TEST_CASE()这个 调用 和 TEST()这个具体测试用例的生成。

难道你愿意在 TEST_GROUP_RUNNER()中 写一堆 TEST_ASSERT_EQUAL()????
反正我不愿意。
强者为尊,弱者,死无葬身之地
点赞  2012-9-14 14:20
事情到了这一步,对这个基本的使用框架,我已经基本理通了。

至于里头实际应用的 UnityMain(),我们暂时不去研究它怎么回事。

但是,我们可以稍微看一下这个函数,实际上它的一系列调用,完成了包括输出格式一类的信息。再往里分析有点累了。

所以,见好就收,我们还是先看看
TEST_ASSERT_EQUAL()这些 我们用户用得着的 宏 和 API再说。

下面这个附件,也是我自己保存的备份。
里头有一个explain.c,是上面几个帖子的完整内容。

另外,刚才因为想开始试一下 第三章的 LedDriver,所以也加进了这个骨架,所以,如果你想下载来试试,千万记得看清楚
改一下 RUN_TEST_GROUP()的名字;

[ 本帖最后由 辛昕 于 2012-9-14 18:05 编辑 ]
强者为尊,弱者,死无葬身之地
点赞  2012-9-14 14:24

问大神::::你说的是啥东西?

一上来就在分析代码,都不知道你在干什么。
点赞  2012-9-19 15:08

回复 8楼 zhouhua1342 的帖子

理解测试是怎么进行的。
强者为尊,弱者,死无葬身之地
点赞  2012-9-19 17:44
如何理解,这的确是个大问题。。
现在我也在问自己,为什么我一上来就分析代码......
强者为尊,弱者,死无葬身之地
点赞  2012-10-5 01:29
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复