软件开发中的理想与现实(二)——让测试驱赶着我们的步伐

2月16日上午,第一天的培训开始。
首先当然应该说说单元测试的必要性,我很欣赏 JUnit In Action这本书里面列的几条理由:
  1. 带来更大的测试范围。单元测试能够更精确地发现问题,能覆盖更广泛的情况,当然,使得项目更可靠。
  2. 带来团队协作的可能。单元测试能够让我们写一点测一点,保证每次提交的质量,而且,团队协作时要是出了问题,找起责任人来也要方便得多。
  3. 防止衰退,减少调试。好的单元测试可以带来自信,也给予我们重构的勇气,同时作为一个副作用,一组好的测试用例能够精确的定位问题,我们或许就省去很多调试的时间。
  4. 使得重构可行。没有单元测试,谁知道我们是在做重构还是在拆房子呢。
  5. 改进实现设计。这个很意外!为了方便测试,开发者会开发出更容易测试的代码,往往这就意味着这些代码更容易维护和更改,而且使得被测函数变得小巧灵活,易于调用。嗯,如果我们真的认真使用一下自己做的东西之后应该就能体会“愚蠢的客户”到底是怎么回事了。
  6. 当作开发者文档来用。测试用例就是这些函数的API!这真是一件奇妙的事情,我们早该想到这点的。
  7. 非常有趣。是的,非常有趣,试试就知道了!
今天的重点是测试驱动开发的体验,为了方便实施自动单元测试,一个成熟易用的测试框架当然必不可少。我们项目采用C++开发, CppUnit当然成了最佳选择。
CppUnit非常好用,特别是它的帮助文档中还有贴心的“CppUnit Cookbook”和“Money, a step by step example”,使得我们很容易入手,不过真正麻烦的是: 何时做测试如何选择测试用例如何选择测试的粒度
既然是测试“驱动”开发,那当然是测试先于开发。具体而言,如果我们有一个类,要实现一个Add方法(功能和它的名字一样),第一步我们会这么做:

class CAddImpl
{
public:
    // Add two numbers
    int Add(int first, int second)
    {
        return 0;
    }
};

然后呢,当然就开始测试(嗯,这么简单的函数居然都不一步写完……是的,这只是举一个例子 ):

class CTestAdd : public CppUnit::TestFixture
{
   
    CPPUNIT_TEST_SUITE(CTestAdd);
    CPPUNIT_TEST(TestAddNormalCase);
    CPPUNIT_TEST_SUITE_END();

    // Test add, normal case
    // 1 + 2 == 3
    void TestAddNormalCase()
    {
        CAddImpl add;
        CPPUNIT_ASSERT_EQUAL(3, add.Add(1, 2));
    }
};

好吧,假设现在CppUnit其他的东西已经准备好了(具体的做法可以看看Cookbook),然后开始运行测试。嗯,失败了,很正常也很必要。OK,我们先改改Add让测试通过再说吧。修改的Add如下所示:

...
    // Add two numbers
    int Add(int first, int second)
    {
        return 3;  // Oh, what a stupid way to implement this...
    }
...

不过不管怎么样,用例通过了。好吧,测试当然是不充分的,比如正数和负数相加会如何呢?嗯,多加一条用例:

...
    // Test add, positive add negtive
    // 2 + (-3) == -1
    void TestAddPositiveAndNegtive()
    {
        CAddImpl add;
        CPPUNIT_ASSERT_EQUAL(-1, add.Add(2, -3));
    }
...

好了好了,我不会再写出愚蠢的代码,我会实现Add了。

...
    // Add two numbers
    int Add(int first, int second)
    {
        return first + second;
    }
...

OK,用例又通过了,太好了。
当然,这不是一个好例子,因为测试和开发的步伐太过于细小了,不过用来说事倒是还算不错。好吧,还是让大家都来做一个小练习。
实现一个CPrime类,它的声明如下:
class CPrime
{
    // Create a pool containing the primes less or equal the number "max"
    // return true if create pool successfully, otherwise, return false
    bool CreatePool(int max);

    // Get a number from the pool
    int GetPrime(int index);
};
为了实现以上CPrime,可以向里面任意添加私有变量和方法。嗯,或许我应该写一个IPrime,不过还是算了吧,简单点是件好事。
在做这个练习的时候大家出了一点问题,现在列在下面,可能是很普遍的错误。
1. 开发步伐过大,把CreatePool写完了才测试。(嗯,这样不行,在大家有这个趋势的时候我就开始阻止了)
2. 不知道如何针对每个方法做覆盖测试。(这是个复杂的话题,如何做到覆盖呢……大体而言,把普通情况、边界情况和异常情况都测试到应该就差不多了,不过真的没有这么简单……)
3. 没有考虑两个两个方法的交互过程。(谁都没有假定CreatePool和Get的顺序,我们应该能应付所有奇怪的用户)
4. 居然没有人问我“the pool”到底是如何存储这些生成的Prime的。(大家都默认为升序排列,而且从index = 0开始,嗯,我可没有这么许诺,我说过我是一个合格的用户)
好了,把所有问题都解决了,大家似乎对测试驱动开发这件事已经有了些基本的了解。嗯,不过,今天的工作也恰好结束了,其他的事情明天再说吧。


参考文献:
[1] Kent Beck,测试驱动开发,中国电力出版社,2004.3
[2] Vincent Massol,JUnit In Action中文版,电子工业出版社,2005.1
[3] Alistair Cockburn,敏捷软件开发,人民邮电出版社,2003.11
[4] http://www.extremeprogramming.org
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值