技术开发 频道

敏捷实践:软件开发中的理想与现实(二)

【IT168 技术文章】    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开始。我可没有这么许诺,我说过我是一个合格的用户)
    好了,把所有问题都解决,大家似乎对测试驱动开发这件事有了些基本的了解。不过,今天的工作也恰好结束,其他的事情明天再说吧。

0
相关文章