2月16日上午,第一天的培训开始。
首先当然应该说说单元测试的必要性,我很欣赏 JUnit In Action这本书里面列的几条理由:
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
首先当然应该说说单元测试的必要性,我很欣赏 JUnit In Action这本书里面列的几条理由:
- 带来更大的测试范围。单元测试能够更精确地发现问题,能覆盖更广泛的情况,当然,使得项目更可靠。
- 带来团队协作的可能。单元测试能够让我们写一点测一点,保证每次提交的质量,而且,团队协作时要是出了问题,找起责任人来也要方便得多。
- 防止衰退,减少调试。好的单元测试可以带来自信,也给予我们重构的勇气,同时作为一个副作用,一组好的测试用例能够精确的定位问题,我们或许就省去很多调试的时间。
- 使得重构可行。没有单元测试,谁知道我们是在做重构还是在拆房子呢。
- 改进实现设计。这个很意外!为了方便测试,开发者会开发出更容易测试的代码,往往这就意味着这些代码更容易维护和更改,而且使得被测函数变得小巧灵活,易于调用。嗯,如果我们真的认真使用一下自己做的东西之后应该就能体会“愚蠢的客户”到底是怎么回事了。
- 当作开发者文档来用。测试用例就是这些函数的API!这真是一件奇妙的事情,我们早该想到这点的。
- 非常有趣。是的,非常有趣,试试就知道了!
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