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