下文将展示一个测试驱动开发(TDD)的实例,希望能给想要开始实践TDD的朋友一个演示。本实例将采用python进行演示,如果您之前没用过python,也不必担心,这是一个很简洁易懂的语言。本人也会在下文实例中对出现的python语法进行解释。
假设python语法中没有 乘法(*) 这个操作符,我们要自己实现一个简单的乘法运算函数。
开始之前我们要记住TDD的核心,那就是:写功能代码之前先写测试,用测试去“驱动”功能实现。换句话说就是“只有在测试失败的时候才能添加或修改功能代码”。具体步骤如下:
- 添加测试。
- 执行测试(测试失败)。
- 实现功能代码。
- 执行测试并通过测试(若测试失败,回到第3步)。
- 回到第一步。
步骤很简单,3~4步可以重复执行n遍直到测试通过。
希望我们可以一起实现这个demo,这样你就能和我一起体验到TDD的乐趣。
- 注意:请确保先安装python,unix系统默认会安装python
执行python --version
,如果打印出python版本则表示已经安装了python。如下:
~ python --version
Python 3.7.2
假设我们要实现一个名为multiply的函数,函数可以输入两个数字参数,返回两个数字相乘的结果。创建一个文件名为tdd-demo.py,我们可以先写如下测试代码:
"""tdd-demo.py"""
import unittest
class MultiplyTest(unittest.TestCase):
def test_multiply(self):
self.assertEqual(multiply(2, 3), 6)
if __name__ == '__main__':
unittest.main()
-
unittest是python用于单元测试的标准库,它提供更人性化的测试结果展示,同时也提供很多测试方法和钩子。本文只用到
assertEqual(a, b)
,意思是断言a == b
,想了解更多关于unittest框架的读者可以点击这里。 - 代码尾部的
if __name__ == '__main__':unittest.main()
代表运行该文件时执行unittest.main()
,既:运行单元测试。
上面测试代码的意思就是测试 multiply(2, 3)
应该等于 6
。
执行 python tdd-demo.py
命令。
➜ python tdd-demo.py
E
======================================================================
ERROR: test_multiply (__main__.MultiplyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "tdd-demo.py", line 7, in test_multiply
self.assertEqual(multiply(2, 3), 6)
NameError: name 'multiply' is not defined
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (errors=1)
执行后返回如上报错,NameError: name 'multiply' is not defined
,意思是multiply这个函数未定义。知道测试失败的原因后,我们添加功能代码如下:
"""tdd-demo.py"""
import unittest
def multiply():
pass
class MultiplyTest(unittest.TestCase):
def test_multiply(self):
self.assertEqual(multiply(2, 3), 6)
if __name__ == '__main__':
unittest.main()
我们定义了一个空白的函数multiply,现在再跑一次测试看看
➜ python tdd-demo.py
E
======================================================================
ERROR: test_multiply (__main__.MultiplyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "tdd-demo.py", line 11, in test_multiply
self.assertEqual(multiply(2, 3), 6)
TypeError: multiply() takes 0 positional arguments but 2 were given
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (errors=1)
上述错误的意思是函数multiply定义了0个传参,但是multiply(2, 3)
传递了2个参数。由此我们知道是忘记给函数multiply定义传参了,我们修改代码如下:
"""tdd-demo.py"""
import unittest
def multiply(a, b):
pass
class MultiplyTest(unittest.TestCase):
def test_multiply(self):
self.assertEqual(multiply(2, 3), 6)
if __name__ == '__main__':
unittest.main()
重新执行 python tdd-demo.py
➜ python tdd-demo.py
F
======================================================================
FAIL: test_multiply (__main__.MultiplyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "tdd-demo.py", line 11, in test_multiply
self.assertEqual(multiply(2, 3), 6)
AssertionError: None != 6
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)
和预期的一样,AssertionError: None != 6
,函数multiply(2, 3)
的结果返回None,没定义函数的返回值当然返回None(python中未定义函数返回值时,则默认返回None),让我们来通过这个测试!
"""tdd-demo.py"""
import unittest
def multiply(a, b):
return 6
class MultiplyTest(unittest.TestCase):
def test_multiply(self):
self.assertEqual(multiply(2, 3), 6)
if __name__ == '__main__':
unittest.main()
上面用了一个取巧的办法,不过现在先别急,我们会在后面修改它,我们先执行测试看看。
➜ python tdd-demo.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
成功了!!
但是我们知道我们的multiply函数现在根本还不能用!每次乘法都返回6的计算器也没人敢用!!
这时候测试都通过了,想要修改功能代码,我们就需要添加新的测试了。
为了节省篇幅,下文只列出部分代码。
class MultiplyTest(unittest.TestCase):
def test_multiply(self):
self.assertEqual(multiply(2, 3), 6)
self.assertEqual(multiply(3, 5), 15)
我们添加了新的测试,再执行看看。
➜ python tdd-demo.py
F
======================================================================
FAIL: test_multiply (__main__.MultiplyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "tdd-demo.py", line 12, in test_multiply
self.assertEqual(multiply(3, 5), 15)
AssertionError: 6 != 15
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)
很好,一切如我们所预料,取巧的办法肯定没法通过完善的测试,乖乖写代码吧。
还记得上文说的,不能够使用 乘法(*) 操作符吗? 但是 加法(+) 是可以使用的,我们知道 2 * 3 = 3 + 3
,也就是2个3相加,乘法a * b
其实是a个b相加。知道原理后我们可以实现代码如下:
def multiply(a, b):
result = 0
while a > 0:
a = a - 1
result = result + b
return result
while a > 0:
表示当 a > 0 时,执行缩进块里的内容既:
a = a - 1
result = result + b
执行测试看看
python tdd-demo.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
成功了!我们用大点的数字相乘看看。
class MultiplyTest(unittest.TestCase):
def test_multiply(self):
self.assertEqual(multiply(2, 3), 6)
self.assertEqual(multiply(3, 5), 15)
def test_multiply_with_larger_number(self):
self.assertEqual(multiply(512, 2), 1024)
self.assertEqual(multiply(10000, 10000), 100000000)
具体的测试代码,可以自行发挥,但是数字不要太大,我们的计算器性能不太好^_^。我们执行测试看看
➜ python tdd-demo.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
成功了哈哈,那如果是传入的参数是负数呢?现在让我添加负数的测试代码看看(ps:记住,想要添加功能前先添加测试代码。能通过测试的代码就是没问题的代码。)
class MultiplyTest(unittest.TestCase):
def test_multiply(self):
self.assertEqual(multiply(2, 3), 6)
self.assertEqual(multiply(3, 5), 15)
def test_multiply_with_larger_number(self):
self.assertEqual(multiply(512, 2), 1024)
self.assertEqual(multiply(10000, 10000), 100000000)
def test_multiply_with_negative_number(self):
self.assertEqual(multiply(-5, 10), -50)
self.assertEqual(multiply(5, -10), -50)
self.assertEqual(multiply(-5, -5), 25)
执行测试
➜ python tdd-demo.py
..F
======================================================================
FAIL: test_multiply_with_negative_number (__main__.MultiplyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "tdd-demo.py", line 23, in test_multiply_with_negative_number
self.assertEqual(multiply(-5, 10), -50)
AssertionError: 0 != -50
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)
果然失败了,AssertionError: 0 != -50
, multiply(-5, 10) 返回了0,让我们看看代码哪里出问题了。找到了 while a > 0
: 因为a = -5 < 0
所以直接返回result = 0
了。知道原因后,我们可以把负数符号先抽出来,让我们改一下代码。(ps:可以自己试着去实现,期间不断地靠测试代码来验证,你会发现有测试代码作保证,功能代码便可以大胆试错,后面你会发现实现功能会比正常开发快很多。)最后我写出如下代码:
def multiply(a, b):
result = 0
is_negative = False
if a < 0:
is_negative = True
a = - a
while a > 0:
a = a - 1
result = result + b
if is_negative:
result = - result
return result
判断 a 是否小于0,如果是的话,就标记一下并把负号抽出来,再把结果添加上负号,是不是很像初学负数运算时的计算步骤。我们再跑测试看看
➜ python tdd-demo.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
成功了!!心思细腻的小朋友可能会发现如果传参a或b为0的时候会怎么样?的确,这样的边界情况没有考虑到,这也是编程让大部分人觉得头疼的地方。我们添加测试看看
class MultiplyTest(unittest.TestCase):
def test_multiply(self):
self.assertEqual(multiply(2, 3), 6)
self.assertEqual(multiply(3, 5), 15)
def test_multiply_with_larger_number(self):
self.assertEqual(multiply(512, 2), 1024)
self.assertEqual(multiply(10000, 10000), 100000000)
def test_multiply_with_negative_number(self):
self.assertEqual(multiply(-5, 10), -50)
self.assertEqual(multiply(5, -10), -50)
self.assertEqual(multiply(-2, -3), 6)
def test_multiply_with_zero_number(self):
self.assertEqual(multiply(0, 5), 0)
self.assertEqual(multiply(2, 0), 0)
self.assertEqual(multiply(0, 0), 0)
执行测试
➜ python tdd-demo.py
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s
OK
居然成功了!自己都没想到,我们回顾一下功能代码,最初定义了result = 0
,a = 0
的时候就直接返回了result
,也就是0
。
到这里,本文的TDD之旅就结束了。如果你不尽兴,可以试着用TDD的方式来实现多个(大于2个)数字相乘,或者当输入的参数不是数字的时候,返回或抛出一些有用的message提示。这也是实际开发中经常用到的场景。
最终的代码如下:
"""tdd-demo.py"""
import unittest
def multiply(a, b):
result = 0
is_negative = False
if a < 0:
is_negative = True
a = - a
while a > 0:
a = a - 1
result = result + b
if is_negative:
result = - result
return result
class MultiplyTest(unittest.TestCase):
def test_multiply(self):
self.assertEqual(multiply(2, 3), 6)
self.assertEqual(multiply(3, 5), 15)
def test_multiply_with_larger_number(self):
self.assertEqual(multiply(512, 2), 1024)
self.assertEqual(multiply(10000, 10000), 100000000)
def test_multiply_with_negative_number(self):
self.assertEqual(multiply(-5, 10), -50)
self.assertEqual(multiply(5, -10), -50)
self.assertEqual(multiply(-2, -3), 6)
def test_multiply_with_zero_number(self):
self.assertEqual(multiply(0, 5), 0)
self.assertEqual(multiply(2, 0), 0)
self.assertEqual(multiply(0, 0), 0)
if __name__ == '__main__':
unittest.main()
ps:本文的目的就是简单介绍一下TDD的实例和步骤,借用乘法multiply的这个函数来演示TDD在具体开发中的实践。具体代码实现肯定有不完善的地方,如果有错误或遗漏的地方,望读者指出。
如果您看完文章,对于TDD有了不一样的认识,或想要在接下来的开发中使用TDD,那么本文就已经完成了它的使命。
最后祝每个程序员都能“控制”代码而不是被代码“控制”,而“控制”代码最好的方式就是用测试代码“控制它”。本文只是以一个简单的乘法函数作为TDD的演示,读者可能在实际的项目开发中会遇到很多测试代码“不好写”的情况。同时迫于项目deadline的压力,想要快速的实现功能并上线,就打算放弃使用TDD的方式。但我也希望你能够保持先写测试的习惯,几个月后,我相信你会感谢自己当时坚持TDD的决定。
扩展阅读:
- 如果python是你的主要开发语言:Python测试驱动开发
- 或许更加通用的书籍:测试驱动开发
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。