近年来测试驱动开发(TDD)受到越来越多的关注。这是一个持续改进的过程,能从一开始就形成规范,帮助提高代码质量。这是切实可行的而非天马行空的。

TDD的全过程是非常简单的。借助TDD,代码质量会得到提升,同时可以让你保持清晰的思路。TDD与敏捷开发可谓强强联合,特别是在进行结对编程的时候。本文主要介绍了TDD的核心概念,还有结合nosetest单元测试包进行Python示例简析。另外还会介绍一些Python备用包。

TDD是什么?

使用该方法可让你少走前人的弯路

顾名思义,TDD即进行编程时先把测试部分写好,当发现不能通过时,再进行编程以使测试通过。然后在这基础上适当地调整测试代码以实现更多功能,最后再编写代码使之实现。

TDD看起来非常像一个环,首先是要不断调整测试代码,然后是编码,改进,最后直至完成。先实现测试部分的做法会使你自然养成把问题放在首位的思维习惯。当真正去构建代码时,就不得不想清楚该如何把设计做好;比方说,该方法有何返回值?当遇到异常时该怎么办?诸如此类。

以这样的方式进行开发,意味着要想出不同的代码实现路径,并在测试中进行实践。这样做可使你少走前人的弯路:陷入一个问题后写出毫不相关的解决方案。

该过程可描述如下:

  • 写出一个缺陷单元测试
  • 使该单元测试通过
  • 重构

与敏捷开发结合

TDD与敏捷开发并行不悖甚至1+1远大于2,这里指的是代码质量而不是数量。

“这意味着结对双方都会参与其中,着重于当前工作,然后在每个环节进行互检。”

然而在结对编程时TDD是单独进行的。如果能把双方的开发流程混合好,互相都能理解就最好不过了。例如,其中一人写出单元测试,当测试通过后,另外一人可以编写不同的测试以之通过。

任何时候结对双方都可以互换角色,每半天或天。这意味着结对双方都会参与其中,每人都把精力放在当前任务上,然后在每个环节进行交叉互检。这难道不是一个双赢的做法吗?

TDD也可以是行为驱动开发过程中的组成部分,同样地,首先写出测试,只不过这里指的是接受测试。这样有助于把工作从头到尾都保持规范。

单元测试语法

进行单元测试时,使用到的Python方法如下:

  • assert: 编写个人声明的基本方式
  • assertEqual(a,b):检查a和b的是否等价
  • assertNotEqual(a,b):检查a和b的是否非等价
  • assertIn(a,b):检查是否存在b中
  • assertNotIn(a,b): 检查是否不存在b中
  • assertFalse(a):检查a的值是否为False
  • assertTrue(a):检查a的值是否为Ture
  • assertIsInstance(a,TYPE):检查a是否为“TYPE”类型
  • assertRaises(ERROR,a,args):以参数args调用a时,检查是否会出现ERROR

以上是实际当中使用频率最高的方法,更多的方法请查阅Python单元测试文档

安装并使用Python Nose

进行下面的练习前,请把nosetest测试运行包安装好。使用标准pip语句进行安装是最直接的做法。此外在项目中使用VirtualEnv(Python虚拟环境)也是不错的做法,因为它可确保所有包在不同项目中是独立的。假如对pip或VirtualEnv了解不多,不妨先查阅相关文档:VirtualEnv,PIP

pip语句十分简洁:

  1. "pip install nose"

安装完成后,可以执行单个测试文件

  1. $ nosetests example_unit_test.py

或者可以直接执行文件夹中的文件组

  1. $ nosetests /path/to/tests

这里要注意的是每个测试方法都应以“test_”为开头,这样nosetest运行机才能正确识别出目标测试文件。

可选参数

下面介绍几个有用的命令行参数:

  • -v:输出更多信息,包括正在执行的测试文件名;
  • -s或-nocapture:进行PRINT语句输出,一般情况下这是隐藏的。开启后可方便调试;
  • --nologcapture:输出日志信息;
  • --rednose:一个可选插件,请点击这里下载,输出带颜色的输出信息;
  • --tags=TAGS:指定要执行的测试文件,而不是整个测试文件组。

实例分析和测试驱动方法

接下来结合一个简单的计算器类例子例如相加/相减,来讲述Python单元测试和TDD概念。对于add相加功能,会尝试编写一个缺陷测试。

在一个空白项目中,首先创建两个python包app和test。然后在每个文件里建立两个名为_init_.py空白文件。这是Phthon工程的标准结构,完成后可以拥有一个可导入的文件结构。如果需要了解更多有关文档架构的信息,请查阅Python包说明文档。 在测试目录里创建一个test_calulator.py文件,其代码如下:

  1. import unittest
  2. class TddInPythonExample(unittest.TestCase):
  3. def test_calculator_add_method_returns_correct_result(self):
  4. calc = Calculator()
  5. result = calc.add(2,2)
  6. self.assertEqual(4, result)

说明:

  • 首先,从Python标准库里导入标准的unittest模块
  • 接着,创建一个含有不同测试用例的类
  • 最后,创建以“test_”为开头的一个测试方法

完成后可着手编写测试代码了。执行方法前要先对计算器进行初始化,初始化完成后便可调用add方法,并把结果存入变量result中。完成后,使用unittest的assertEqual方法来确保add方法正常执行。

现在可以启动nosetest来执行测试文件了。代码如下:

  1. if __name__ == '__main__':
  2. unittest.main()

标准的Python文件执行方式为$ python test_calculator.py,相比之下本文使用的nosetests方法功能更丰富,例如可以运行目录中的全部测试文件。

  1. $ nosetests test_calculator.py
  2. E
  3. ======================================================================
  4. ERROR: test_calculator_add_method_returns_correct_result (test.test_calculator.TddInPythonExample)
  5. ----------------------------------------------------------------------
  6. Traceback (most recent call last):
  7. File "/Users/user/PycharmProjects/tdd_in_python/test/test_calculator.py", line 6, in test_calculator_add_method_returns_correct_result
  8. calc = Calculator()
  9. NameError: global name 'Calculator' is not defined
  10. ----------------------------------------------------------------------
  11. Ran 1 test in 0.001s
  12. FAILED (errors=1)

运行后可见出错的原因是没有导入Caculator。因为还没有创建呢!创建的方法是在app目录下建立calculator.py文件,然后导入:

  1. class Calculator(object):
  2. def add(self, x, y):
  3. pass
  1. import unittest
  2. from app.calculator import Calculator
  3. class TddInPythonExample(unittest.TestCase):
  4. def test_calculator_add_method_returns_correct_result(self):
  5. calc = Calculator()
  6. result = calc.add(2,2)
  7. self.assertEqual(4, result)
  8. if __name__ == '__main__':
  9. unittest.main()

把Caculator构建好之后,再次运行看会出现什么结果:

  1. $ nosetests test_calculator.py
  2. F
  3. ======================================================================
  4. FAIL: test_calculator_add_method_returns_correct_result (test.test_calculator.TddInPythonExample)
  5. ----------------------------------------------------------------------
  6. Traceback (most recent call last):
  7. File "/Users/user/PycharmProjects/tdd_in_python/test/test_calculator.py", line 9, in test_calculator_add_method_returns_correct_result
  8. self.assertEqual(4, result)
  9. AssertionError: 4 != None
  10. ----------------------------------------------------------------------
  11. Ran 1 test in 0.001s
  12. FAILED (failures=1)

很明显,add方法返回了错误的值,因为还没有为它指定行为。幸好nosetest会指出出错的位置,方便进行修改。稍作改动后,测试便可通过了:

  1. class Calculator(object):
  2. def add(self, x, y):
  3. return x+y
  1. $ nosetests test_calculator.py
  2. .
  3. ----------------------------------------------------------------------
  4. Ran 1 test in 0.000s
  5. OK

虽然通过了,但是围绕该方法还可以做更多的工作。

沉迷于某个案例很容易造成短视

如果进行非数字型数据相加会导致什么后果呢?事实上Python是允许字符串或其它类型进行相加的,但在我们的例子里不允许。接着尝试就这个例子加入另一个缺陷测试,然后使用assertRaises方法来判断是否有异常抛出:

  1. import unittest
  2. from app.calculator import Calculator
  3. class TddInPythonExample(unittest.TestCase):
  4. def setUp(self):
  5. self.calc = Calculator()
  6. def test_calculator_add_method_returns_correct_result(self):
  7. result = self.calc.add(2, 2)
  8. self.assertEqual(4, result)
  9. def test_calculator_returns_error_message_if_both_args_not_numbers(self):
  10. self.assertRaises(ValueError, self.calc.add, 'two', 'three')
  11. if __name__ == '__main__':
  12. unittest.main()

以上代码中,检查了是否引起了ValueError错误,其实还可以进行更多的检测,不过在这里不作深入讲述。此外,setup()方法用于推入计算对象。下面再看看nosetest会反馈什么信息:

  1. $ nosetests test_calculator.py
  2. .F
  3. ======================================================================
  4. FAIL: test_calculator_returns_error_message_if_both_args_not_numbers (test.test_calculator.TddInPythonExample)
  5. ----------------------------------------------------------------------
  6. Traceback (most recent call last):
  7. File "/Users/user/PycharmProjects/tdd_in_python/test/test_calculator.py", line 15, in test_calculator_returns_error_message_if_both_args_not_numbers
  8. self.assertRaises(ValueError, self.calc.add, 'two', 'three')
  9. AssertionError: ValueError not raised
  10. ----------------------------------------------------------------------
  11. Ran 2 tests in 0.001s
  12. FAILED (failures=1)

显然nosetests告诉我们ValueError没有被抛出。现在我们有了一个新的缺陷测试,接着尝试编码进行解决:

  1. class Calculator(object):
  2. def add(self, x, y):
  3. number_types = (int, long, float, complex)
  4. if isinstance(x, number_types) and isinstance(y, number_types):
  5. return x + y
  6. else:
  7. raise ValueError

代码中使用了isinstance方法是为了确保输入的是数字型数据。

由于两个变量的类型有多种组合,为了进行完整的测试,所以需要把可能出现的组合进行统筹并进行处理:

  1. import unittest
  2. from app.calculator import Calculator
  3. class TddInPythonExample(unittest.TestCase):
  4. def setUp(self):
  5. self.calc = Calculator()
  6. def test_calculator_add_method_returns_correct_result(self):
  7. result = self.calc.add(2, 2)
  8. self.assertEqual(4, result)
  9. def test_calculator_returns_error_message_if_both_args_not_numbers(self):
  10. self.assertRaises(ValueError, self.calc.add, 'two', 'three')
  11. def test_calculator_returns_error_message_if_x_arg_not_number(self):
  12. self.assertRaises(ValueError, self.calc.add, 'two', 3)
  13. def test_calculator_returns_error_message_if_y_arg_not_number(self):
  14. self.assertRaises(ValueError, self.calc.add, 2, 'three')
  15. if __name__ == '__main__':
  16. unittest.main()

至此我们可以运行所有的测试了,所要实现的需求也都满足了。

其它的单元测试包

py.test

pytest的作用与nosetest类似,不过可以在单独的区域里输出信息,这意味着能够使我们很快地看清楚命令行中出现的打印信息。这对于只运行单个测试的情况是很有用的。

  1. $ nosetests test_calculator.py
  2. ....
  3. ----------------------------------------------------------------------
  4. Ran 4 tests in 0.001s
  5. OK

安装pytest的方式与nosetest差不多,命令是$ pip install pytes。执行的命令是$ pip install pytes或者指定要执行的测试文件$ py.test test/calculator_tests.py。

  1. $ py.test test/test_calculator.py
  2. ================================================================= test session starts =================================================================
  3. platform darwin -- Python 2.7.6 -- py-1.4.26 -- pytest-2.6.4
  4. collected 4 items
  5. test/test_calculator.py ....
  6. ============================================================== 4 passed in 0.02 seconds ===============================================================

pytest运行后的结果如下。注:只有代码含有错误或异常的情况下,pytest才会进行输出。

  1. $ py.test test/test_calculator.py
  2. ================================================================= test session starts =================================================================
  3. platform darwin -- Python 2.7.6 -- py-1.4.26 -- pytest-2.6.4
  4. collected 4 items
  5. test/test_calculator.py F...
  6. ====================================================================== FAILURES =======================================================================
  7. ________________________________________ TddInPythonExample.test_calculator_add_method_returns_correct_result _________________________________________
  8. self = <test.test_calculator.TddInPythonExample testMethod=test_calculator_add_method_returns_correct_result>
  9. def test_calculator_add_method_returns_correct_result(self):
  10. result = self.calc.add(3, 2)
  11. >       self.assertEqual(4, result)
  12. E       AssertionError: 4 != 5
  13. test/test_calculator.py:11: AssertionError
  14. ---------------------------------------------------------------- Captured stdout call -----------------------------------------------------------------
  15. X value is: 3
  16. Y value is: 2
  17. Result is 5
  18. ========================================================= 1 failed, 3 passed in 0.03 seconds ==========================================================

单元测试

如果不想安装额外的包并想保持一个纯净的标准库结构,使用Python内建的unittest单元测试包是不错的选择。其使用方法如下:

  1. if __name__ == '__main__':
  2. unittest.main()

使用python calculator_tests.py执行后,看会得到什么结果:

  1. $ python test/test_calculator.py
  2. ....
  3. ----------------------------------------------------------------------
  4. Ran 4 tests in 0.004s
  5. OK

使用PDB进行调试

以TDD方式开发,经常会遇到来自代码或测试的问题。有时这些错误又是比较隐蔽的。因此,需要配合使用高明的调试技术。

以TDD方式进行开发出现问题时可能难以发现

幸运地,有不少的办法来解决这些问题。其中最简单的方式是透过增添print语句实现“断点”输出。

结合print语句进行调试

加法通过后,可以尝试进行减法调试。把app/calculator.py中的add部分代码作如下改动:

  1. class Calculator(object):
  2. def add(self, x, y):
  3. number_types = (int, long, float, complex)
  4. if isinstance(x, number_types) and isinstance(y, number_types):
  5. return x - y
  6. else:
  7. raise ValueError

这里不妨尝试使用print语句进行输出,来监视值是怎样变化的。

  1. class Calculator(object):
  2. def add(self, x, y):
  3. number_types = (int, long, float, complex)
  4. if isinstance(x, number_types) and isinstance(y, number_types):
  5. print 'X is: {}'.format(x)
  6. print 'Y is: {}'.format(y)
  7. result = x - y
  8. print 'Result is: {}'.format(result)
  9. return result
  10. else:
  11. raise ValueError

现在可以使用nosetest来执行并查看结果,可见这样的工整输出结构,对调试是十分有帮助的。

  1. $ nosetests test/test_calculator.py
  2. F...
  3. ======================================================================
  4. FAIL: test_calculator_add_method_returns_correct_result (test.test_calculator.TddInPythonExample)
  5. ----------------------------------------------------------------------
  6. Traceback (most recent call last):
  7. File "/Users/user/PycharmProjects/tdd_in_python/test/test_calculator.py", line 11, in test_calculator_add_method_returns_correct_result
  8. self.assertEqual(4, result)
  9. AssertionError: 4 != 0
  10. -------------------- >> begin captured stdout << ---------------------
  11. X is: 2
  12. Y is: 2
  13. Result is: 0
  14. --------------------- >> end captured stdout << ----------------------
  15. ----------------------------------------------------------------------
  16. Ran 4 tests in 0.002s
  17. FAILED (failures=1)

PDB进阶调试

如果遇到更复杂的调试环节,仅仅依靠print语句是不够的。其中最经常使用的进阶调试工具是pdb(Python Debugger)。该工具包含在标准库中,使用的时候只需加入一行代码到“断点”位置。请看下面的代码:

  1. class Calculator(object):
  2. def add(self, x, y):
  3. number_types = (int, long, float, complex)
  4. if isinstance(x, number_types) and isinstance(y, number_types):
  5. import pdb; pdb.set_trace()
  6. return x - y
  7. else:
  8. raise ValueError

请注意,如果使用nosetest执行测试,请务必使用-s标记,否则nosetest会继续对输出进行抓取,这样会使pdb无法正常运行。如果是使用unittest或pytest则无需这样做。

如果测试停止并有pdb提示,请使用list命令来进行当前代码定位。

  1. $ nosetests -s
  2. > /Users/user/PycharmProjects/tdd_in_python/app/calculator.py(7)add()
  3. -> return x - y
  4. (Pdb) list
  5. 2          def add(self, x, y):
  6. 3             number_types = (int, long, float, complex)
  7. 4
  8. 5             if isinstance(x, number_types) and isinstance(y, number_types):
  9. 6                 import pdb; pdb.set_trace()
  10. 7  ->              return x - y
  11. 8             else:
  12. 9                 raise ValueError
  13. [EOF]
  14. (Pdb)

出现提示后是可以进行交互操作的,比方说想在这个时候检阅x和y的值:

  1. (Pdb) x
  2. 2
  3. (Pdb) y
  4. 2

如果想了解更多命令,可以键入help来查看。经常使用的命令如下所示:

  • n: 步进到下个执行
  • list: 显示当前位置
  • args: 显示在当前执行点上用到的变量
  • continue:运行代码直至结束
  • jump <line number>: 运行并跳转到行号位置
  • quit/exit:停止pdb

写在最后

TDD模式十分有趣同时能帮助提高代码质量。不论是大型团队还是个人开发,TDD都可运用其中。此外,成功的缺陷测试设计是非常有满足感的。所以,不妨从今天起尝试把TDD引入到日常工作中,亲身体验试验前后会有什么变化。

英文来自:code.tutsplus

http://www.csdn.net/article/2015-02-16/2823992-python/2

基于Python的测试驱动开发实战的更多相关文章

  1. (数据科学学习手札47)基于Python的网络数据采集实战(2)

    一.简介 马上大四了,最近在暑期实习,在数据挖掘的主业之外,也帮助同事做了很多网络数据采集的内容,接下来的数篇文章就将一一罗列出来,来续写几个月前开的这个网络数据采集实战的坑. 二.马蜂窝评论数据采集 ...

  2. 学习Keras:《Keras快速上手基于Python的深度学习实战》PDF代码+mobi

    有一定Python和TensorFlow基础的人看应该很容易,各领域的应用,但比较广泛,不深刻,讲硬件的部分可以作为入门人的参考. <Keras快速上手基于Python的深度学习实战>系统 ...

  3. (数据科学学习手札33)基于Python的网络数据采集实战(1)

    一.简介 前面两篇文章我们围绕利用Python进行网络数据采集铺垫了很多内容,但光说不练是不行的,于是乎,本篇就将基于笔者最近的一项数据需求进行一次网络数据采集的实战: 二.网易财经股票数据爬虫实战 ...

  4. 关于狗书《Flask web开发 基于python的Web开发应用实战》中加入用户隐私功能

    目前是第二次撸狗书,在用户页面这一块我个人觉得有些问题(基于交互设计).按理来说,我作为一个权限只有User的个人用户来说,肯定不喜欢让别人看到我的真实姓名,地址之类的敏感信息.所以我应该是可以设置成 ...

  5. 基于Python的接口自动化实战-基础篇之读写配置文件

    引言 在编写接口自动化测试脚本时,有时我们需要在代码中定义变量并给变量固定的赋值.为了统一管理和操作这些固定的变量,咱们一般会将这些固定的变量以一定规则配置到指定的配置文件中,后续需要用到这些变量和变 ...

  6. 基于Python的接口自动化实战-基础篇之pymysql模块操作数据库

    引言 在进行功能或者接口测试时常常需要通过连接数据库,操作和查看相关的数据表数据,用于构建测试数据.核对功能.验证数据一致性,接口的数据库操作是否正确等.因此,在进行接口自动化测试时,我们一样绕不开接 ...

  7. 1.关于狗书《Flask Web开发 基于Python的web开发应用实战》身份验证的改进

    在我学习用户身份验证的时候,我发现这里有个小弊端,在用户注册完成后想要验证邮箱的时候,点击邮箱中的网址进行验证,此时还要登陆,这及其不符合我们的习惯.一般情况下我们只需要点击网址就可以验证成功并且进入 ...

  8. 原创翻译-测试驱动开发(TDD)

    测试驱动开发原则 翻译自<<Expert Python Programming>> 测试驱动开发是指首先编写包含所有测试软件特点的测试集,然后再去开发软件.也就是说,在编写软件 ...

  9. (数据科学学习手札50)基于Python的网络数据采集-selenium篇(上)

    一.简介 接着几个月之前的(数据科学学习手札31)基于Python的网络数据采集(初级篇),在那篇文章中,我们介绍了关于网络爬虫的基础知识(基本的请求库,基本的解析库,CSS,正则表达式等),在那篇文 ...

随机推荐

  1. 在linux下导入.sql文件,数据库中文乱码

    现象描述 我是在aix下面导入如下SQL语句时,数据库中显示乱码. insert into CONFERENCE(CONFERENCEID,SUBCONFERENCEID,ACCESSNUMBER,A ...

  2. python 报错 SyntaxError: Non-ASCII character

    报错: SyntaxError: Non-ASCII character 概意思是,默认文件是ASCII格式,需要更改文件编码,操作是在文件首行加上 #!/usr/bin/python # -*- c ...

  3. java中异常和集合

    1. java中处理错误情况有两种,1  Error,2  Exception error是无法处理的,Exception是可以处理的情况. Exception中又有两种情况,RuntimeExcep ...

  4. linux系统中用户切换

    1. Linux系统中用户切换的命令为su,语法为: su [-fmp] [-c command] [-s shell] [--help] [--version] [-] [USER [ARG]] 参 ...

  5. JavaScript:零星知识

    1. 关于document.write() 如果在文档已完成加载后执行 document.write,整个HTML 页面将被覆盖. 2. 对代码行进行折行 您可以在文本字符串中使用反斜杠对代码行进行换 ...

  6. php四种排序算法实现代码

    分享php排序的四种算法与代码. 冒泡:function bubble_sort($arr){ $num = count($arr); for($i=0;$i<$num;$i++){ for($ ...

  7. novas的verdi和debussy是干什么用的(关于debussy的一些介绍)

    source code window: 提供了一个比较友好的界面,将整个设计的source code按设计的层次结构以树状排布,并且可以在代码上反标仿真结果,支持查找.寻找驱动等一些debug常用的操 ...

  8. 利用ItextSharp 生成PDF文档改进版

    导入的ItextSharp.dll一定要是较高的版本 数据库表结构 生成的PDF样式 代码: namespace WebPDF { public partial class _Default : Sy ...

  9. vue使用sweetalert2弹窗插件

    1). 安装 sweetalert2 npm install sweetalert2@7.15.1 --save 2). 封装 sweetalert2 在 src 新建 plugins 文件夹,然后新 ...

  10. RSS Feeds with Spring Boot

    http://nixmash.com/post/rss-feeds-with-spring-boot **************************************** We added ...