zoukankan      html  css  js  c++  java
  • 基于Python的测试驱动开发实战

    近年来测试驱动开发(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语句十分简洁:

    [py] view plaincopy
     
    1. "pip install nose"  

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

    [py] view plaincopy
     
    1. $ nosetests example_unit_test.py  

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

    [py] view plaincopy
     
    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文件,其代码如下:

    [py] view plaincopy
     
    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来执行测试文件了。代码如下:

    [py] view plaincopy
     
    1. if __name__ == '__main__':  
    2.     unittest.main()  

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

    [py] view plaincopy
     
    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. ----------------------------------------------------------------------  
    12. Ran 1 test in 0.001s  
    13.    
    14. FAILED (errors=1)  

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

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

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

    [py] view plaincopy
     
    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. ----------------------------------------------------------------------  
    12. Ran 1 test in 0.001s  
    13.    
    14. FAILED (failures=1)  

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

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

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

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

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

    [py] view plaincopy
     
    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会反馈什么信息:

    [py] view plaincopy
     
    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. ----------------------------------------------------------------------  
    12. Ran 2 tests in 0.001s  
    13.    
    14. FAILED (failures=1)  

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

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

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

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

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

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

    其它的单元测试包

    py.test

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

    [py] view plaincopy
     
    1. $ nosetests test_calculator.py  
    2. ....  
    3. ----------------------------------------------------------------------  
    4. Ran 4 tests in 0.001s  
    5.    
    6. OK  

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

    [py] view plaincopy
     
    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.    
    6. test/test_calculator.py ....  
    7.    
    8. ============================================================== 4 passed in 0.02 seconds ===============================================================  

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

    [py] view plaincopy
     
    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.    
    6. test/test_calculator.py F...  
    7.    
    8. ====================================================================== FAILURES =======================================================================  
    9. ________________________________________ TddInPythonExample.test_calculator_add_method_returns_correct_result _________________________________________  
    10.    
    11. self = <test.test_calculator.TddInPythonExample testMethod=test_calculator_add_method_returns_correct_result>  
    12.    
    13.     def test_calculator_add_method_returns_correct_result(self):  
    14.         result = self.calc.add(3, 2)  
    15. >       self.assertEqual(4, result)  
    16. E       AssertionError: 4 != 5  
    17.    
    18. test/test_calculator.py:11: AssertionError  
    19. ---------------------------------------------------------------- Captured stdout call -----------------------------------------------------------------  
    20. X value is: 3  
    21. Y value is: 2  
    22. Result is 5  
    23. ========================================================= 1 failed, 3 passed in 0.03 seconds ==========================================================  

    单元测试

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

    [py] view plaincopy
     
    1. if __name__ == '__main__':  
    2.     unittest.main()  

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

    [py] view plaincopy
     
    1. $ python test/test_calculator.py   
    2. ....  
    3. ----------------------------------------------------------------------  
    4. Ran 4 tests in 0.004s  
    5.    
    6. OK  

    使用PDB进行调试

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

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

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

    结合print语句进行调试

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

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

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

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

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

    [py] view plaincopy
     
    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. is: 2  
    12. is: 2  
    13. Result is: 0  
    14.    
    15. --------------------- >> end captured stdout << ----------------------  
    16.    
    17. ----------------------------------------------------------------------  
    18. Ran 4 tests in 0.002s  
    19.    
    20. FAILED (failures=1)  

    PDB进阶调试

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

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

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

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

    [py] view plaincopy
     
    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的值:

    [py] view plaincopy
     
    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

  • 相关阅读:
    Educational Codeforces Round 86 (Rated for Div. 2) D. Multiple Testcases
    Educational Codeforces Round 86 (Rated for Div. 2) C. Yet Another Counting Problem
    HDU
    HDU
    HDU
    HDU
    Good Bye 2019 C. Make Good (异或的使用)
    Educational Codeforces Round 78 (Rated for Div. 2) C. Berry Jam
    codeforces 909C. Python Indentation
    codeforces1054 C. Candies Distribution
  • 原文地址:https://www.cnblogs.com/awishfullyway/p/6393510.html
Copyright © 2011-2022 走看看