Sunday, March 05, 2006

A Unit Testing Toy

Unit Testing 的 framework,最早是由 Kent Beck 在〈Simple Smalltalk Testing〉這篇 paper 中提出。後來因為 Java 的流行,及 Java 和 Smalltalk 的相似性,Kent Beck 又完成了一個 Java 版本的 Unit Testing framework -- JUnit 。隨著 Extreme Programming 的熱門,以及 test-driven development 在實務上的重大成功,現在幾乎各種程式語言都有它們的 Unit Testing Framework ,大家都來 Unit 一下,並通稱為 XUnit

還記得我曾經為一個簡單的 Prolog Interpreter ,在 C++ 下找來了 Unit++ 來作 Unit Test,以下是當時寫的 test suit:

 1: /**
 2:  * @file utest.cpp
 3:  * @author Jiang Yu-Kuan
 4:  * @date 2003/12/23
 5:  */
 6: #include <unit++.h>
 7: #include "logic.h"
 8: using namespace std;
 9: using namespace unitpp;
10: 
11: namespace
12:  {
13:     class Test : public suite
14:     {
15:         ostringstream ost;
16:         void mathObject() {
17:             Variable v1("X1"), v2("X1");
18:             ost.str(""); // clear the containing string
19:             ost << v1 << " " << (v1==v2) << " " << v2;
20:             assert_eq( "variable check", "X1 1 X1",  ost.str() );
21: 
22:             Constant cs1("cc"), cs2("cc");
23:             ost.str("");
24:             ost << cs1 << " " << (cs1==cs2) << " " << cs2;
25:             assert_eq( "constant check", "cc 1 cc",  ost.str() );
26: 
27:             List l1("(a,b,f(a,g(c)))"), l2("(a,b,f(a,g(c)))");
28:             ost.str("");
29:             ost << l1 << " " << (l1==l2) << " " << l2;
30:             assert_eq( "list check", "(a, b, f(a, g(c))) 1 (a, b, f(a, g(c)))",  ost.str() );
31: 
32:             Compound cp("f(a,b,c,f(Z,g(x)),g(y))");
33:             ost.str("");
34:             ost << cp;
35:             assert_eq( "compound check", "f(a, b, c, f(Z, g(x)), g(y))",  ost.str() );
36: 
37:             ost.str("");
38:             ost << isCompound(cp);
39:             ost << isList(l1) << isList(v1) << isList(cs1);
40:             assert_eq( "isTerm check", "1100",  ost.str() );
41:         }
42: 
43:         Substitution s;
44:         Compound a, b;
45:         void unifyCase1() {
46:             ost.str("");
47:             ost << a << " " << Unify(a, b, s) << " " << b;
48:             assert_eq( "compound/unify check", "f(X11, g(b)) 1 f(a, Y1234)",  ost.str() );
49: 
50:             ost.str("");
51:             ost << s;
52:             assert_eq( "meat of substitution check", "{ X11/a, Y1234/g(b) }",  ost.str() );
53:         }
54:         void substituteCase1() {
55:             Substitute( a, s );
56:             ost.str("");
57:             ost << a;
58:             assert_eq( "substitute check", "f(a, g(b))",  ost.str() );
59:         }
60: 
61:         Compound c, d;
62:         void unifyCase2() {
63:             s.clear();
64:             ost.str("");
65:             ost << c << " " << Unify(c, d, s) << " " << d;
66:             assert_eq( "compound/unify check", "f(X, f(X, Y)) 1 f(g(Y), f(g(a), Z))",  ost.str() );
67: 
68:             ost.str("");
69:             ost << s;
70:             assert_eq( "meat of substitution check", "{ X/g(a), Y/a, Z/a }",  ost.str() );
71:         }
72:         void substituteCase2() {
73:             Substitute( c, s );
74:             ost.str("");
75:             ost << c;
76:             assert_eq( "substitute check", "f(g(a), f(g(a), a))",  ost.str() );
77:         }
78:     public:
79:         Test():
80:              suite("Unify test suite"),
81:              a("f(X11, g(b))"), b("f(a, Y1234)"),
82:              c("f(X, f(X, Y))"), d("f(g(Y), f(g(a), Z))")
83:         {
84:             add("mathObject", testcase(this, "Math Object", &Test::mathObject));
85:             add("unifyCase1", testcase(this, "Unify Case1", &Test::unifyCase1));
86:             add("substituteCase1", testcase(this, "Substitute Case1", &Test::substituteCase1));
87:             add("unifyCase2", testcase(this, "Unify Case2", &Test::unifyCase2));
88:             add("substituteCase2", testcase(this, "Substitute Case2", &Test::substituteCase2));
89:             suite::main().add("UnifyTestSuite", this);
90:         }
91:     } * theTest = new Test();
92: }

後來有好一陣子,我工作都是在開發 Embedded System 的程式,大多數的情況下只能用 C ,且使用的是 8 bit 的 MCU ,記憶體是受限的(以 K 為單位在計算)。不用說先前我為開發 C++ 程式找的 Unit++ 不能用,就連許多專為 C 設計的 unit testing framework 也顯得太臃腫了。這可怎麼辦?難道放棄 Unit Test 嗎?這對開發 Embedded System 而言,太冒險了;改成手動 Unit Test 呢?又太累人了!

在看了 MinUnit -- a minimal unit testing framework for C 後,我受到了很大的啟示:framework 只是個工具,重點是「測試」和「自動化」。在融合了 Unit++ 的介面後,我為 C 寫了一個在系統資源受限的情況下,也能適用的 Unit Testing Toy:

 1: /**
 2:  * @file ToyUnit.h
 3:  *      \em ToyUnit -- A toy unit testing framework for C Language.
 4:  * @attention \em ToyUnit is designed for non-file-system environment,
 5:  *      e.g. the Keil C51. Hence, \em buffer \em flushing (for stdout/stderr)
 6:  *      is not necessary upon the implementing of \c TU_ASSERT()
 7:  *      and \c TU_RESULT().
 8:  * @warning It is necessary to call flush() while applying
 9:  *      \em ToyUnit with \em buffer I/O platform.
10:  * @author Jiang Yu-Kuan
11:  * @date 2005/3/9
12:  * @version 1.1
13:  * @see MinUnit (http://www.jera.com/techinfo/jtns/jtn002.html)
14:  * @see ToyUnit_test.c
15:  * @todo to extend the framework to fit the more general environment
16:  */
17: #ifndef _TOY_UNIT_H_
18: #define _TOY_UNIT_H_
19: 
20: 
21: #if !defined( NDEBUG )
22: 
23:     #include <stdio.h>
24: 
25:     static unsigned int _TU_PASS_RUN= 0; ///< the number of pass test runs
26:     static unsigned int _TU_FAIL_RUN= 0; ///< the number of fail test runs
27: 
28:     /** Asserts that the \a assertion is true.
29:      * @param msg a message that is shown if asserting fail
30:      * @param assertion a boolean expression
31:      */
32:     #define TU_ASSERT( msg, assertion ) \
33:         do {                            \
34:             if (assertion) {            \
35:                 putchar('.');           \
36:                 _TU_PASS_RUN++;         \
37:             }                           \
38:             else {                      \
39:                 puts("\n" msg " [test fail]");\
40:                 _TU_FAIL_RUN++;         \
41:             }                           \
42:         } while (0)
43: 
44:     /** Prints the statistics of pass runs and fail runs. */
45:     #define TU_RESULT() \
46:         printf("\nTests [Pass-Fail]: [%u-%u]", _TU_PASS_RUN, _TU_FAIL_RUN)
47: 
48: #else
49: 
50:     #define TU_ASSERT( msg, assertion ) ((void)0)
51:     #define TU_RESULT() ((void)0)
52: 
53: #endif // NDEBUG
54: 
55: 
56: #endif // _TOY_UNIT_H_
57: 
58: /** @example ToyUnit_test.c
59:  *      This is an example of how to use the ToyUnit testing framework.
60:  */

使用時只要簡單地 include 上面這個 ToyUnit.h 即可。以下是這個 ToyUnit 的 Unit Test:

 1: /**
 2:  * @file ToyUnit_test.c
 3:  *      An example to show how to use the \em ToyUnit testing framework
 4:  * @author Jiang Yu-Kuan
 5:  * @date 2005/3/9
 6:  * @see ToyUnit.h
 7:  */
 8: #include "ToyUnit.h"
 9: 
10: int main()
11: {
12:     TU_ASSERT("test 1", 1==1);
13:     TU_ASSERT("test 2", 1+1!=2); // test fail
14:     TU_ASSERT("test 3", 1+1==2);
15:     TU_ASSERT("test 4", 1*1==1);
16:     TU_ASSERT("test 5", 1-1==0);
17:     TU_ASSERT("test 6", 2==2);
18:     TU_ASSERT("test 7", 2+1==3);
19:     TU_ASSERT("test 8", 1==1);
20:     TU_RESULT(); // Tests [Pass-Fail]: [7-1]
21: 
22:     return 0;
23: }

嘻!它被拿來自己測試自己,也剛好當成使用範例 :)

3 comments:

York said...

最近把這篇原本放在 frame tag 的 code 改成良人大密寶裡面,以 code tag 搭配 CSS 的方式來呈現 code 。

在 Firefox 上看起來還不賴,可惜 IE 不支援 CSS 的 max-height 等 attributes ,且其字型呈現上,如字的大小等,也跟 Firefox 差很多,所以在 IE 下看起來效果滿差的。

Alan Yeh said...

目前我Unit Test的做法是先把要測試的lib, test case and test main.c用gcc compile成在host端可執行檔, 待測試成功後, 再用arm-elf-gcc 將lib compile成target device要使用的.a

能否請教您是怎樣測試你的 MCU code 呢?

York said...

To bFish,

針對支援 GCC 的 embedded 開發環境,用你的方法也就夠了。

否則,還可以在 MCU 的 simulator 執行部份 Unit Tests ,畢竟 C 在不同平台下的行為還是有細微的差異。

對消費級的產品來說,我只會在重要的或易出錯的程式片段作較完整的 Unit Tests 。

其餘較 trivial 的部份,除了撰碼時謹慎些外,剩下的就等功能測試出錯時在來針對可疑點搭建測試碼。