在Python中使用测试驱动开发

测试驱动开发(TDD)是一个近些年被实践证明过的过程。把测试带入日常的编码过程而不是编码完成后才进行测试的过程应该是开发人员试图成为习惯的方式而不是空谈的方式。

测试驱动开发的整个过程是很容易被掌握的,而且给我们带来很多的好处--代码质量的提高,但是也清晰和专注于你要达到的目标它是什么以及你要怎样达到目标。测试驱动开发也可以无缝地与敏捷开发一起工作,在结对编程的时候,能够充分被利用,你将在后面会看到。

在本教程中,我将会介绍测试驱动开发的核心概念,提供使用python的nosetests单元测试包的相关例子。另外,我将提供一些可选的软件包。

什么是测试驱动开发?

这里采用了百度百科的测试驱动开发词条的含义:
测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法。它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。这有助于编写简洁可用和高质量的代码,并加速开发过程。

Kent Beck先生最早在其极限编程(XP)方法论中,向大家推荐“测试驱动”这一最佳实践,还专门撰写了《测试驱动开发》一书,详细说明如何实现。经过几年的迅猛发展,测试驱动开发已经成长为一门独立的软件开发技术,其名气甚至盖过了极限编程。


如上图所示,测试驱动开发的过程如下:
1.编写一个失败的单元测试用例;
2.编写代码使得单元测试用例通过;
3.重构
如果必要的话,为每一个可能,重复此过程。

敏捷开发与测试驱动开发

简单的说,敏捷开发是一种以人为核心、迭代、循序渐进的开发方法。在敏捷开发中,软件项目的构建被切分成多个子项目,各个子项目的成果都经过测试,具备集成和可运行的特征。测试驱动开发是一个与敏捷开发过程中的理想和原则的完美匹配,是一个努力提供增量更新的产品的真正的质量,而不是数量。测试驱动开发是敏捷开发中的一项核心实践和技术,也是一种设计方法论。

单元测试的语法

我们使用Python的单元测试的主要方法是:
assert-基础断言,允许用户自己扩展断言;
assertEqual(a, b)-判断a和b是否相等;
assertNotEqual(a, b)-检查a和b是否不相等;
assertIn(a, b)-检查a是否在b中;
assertNotIn(a, b)-检查a是否不在b中;
assertFalse(a)-检查a的值是否是False;
assertTrue(a)-检查a的值是否是True;
assertIsInstance(a, TYPE)-检查a的类型是否是“TYPE”;
assertRaises(ERROR, a, args)-检查当a使用参数args被调用的时候是否会引发ERROR;

也有些其它的方法提供给我们,您可以查看- Python单元测试文档的 -但是,在我的经验中,上面列出的是最常用的。下面我们将利用这些在我们的例子中。

用python测试驱动开发的例子

我们将要看到一个真正简单的例子,这个例子用来介绍使用python单元测试和测试驱动开发的概念。我们将编写一个非常简单的计算器类,包含加,减和其他简单的方法,你会期望的。按照测试驱动开发的做法,让我们说,我们有一个要求添加功能,这将决定两个数字,并返回输出。让我们来写一个失败的测试。

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

 写这个测试十分简单:
 1.首先需要从python的标准库中导入unittest模块;
 2.接着,我们需要写一个包含不同测试用例的类;
 3.最后,需要编写一个测试的函数,唯一的要求是函数名是以“test_”开头,这样能够被nosetest识别以及执行,我们将在后面详细介绍。

 我们可以编写具体的测试代码在函数中的位置。我们初始化我们的计算器,这样我们就可以执行它的方法。在此之后,我们就可以调用add的方法,这是我们希望测试,其输出值存储在变量result中。一旦完成,我们就可以进行单元测试的assertEqual方法的使用,以确保我们的计算器的add方法的行为如预期一样。

 现在如果我们运行测试代码,将会看到下面的失败信息:


从nosetest输出的结果来看,我们的代码中没有导入Calculator。这是因为我们根本还没有创建它!因此我们定义Calculator以及导入它:

class Calculator(object):
 
   def add(self, x, y):
      pass

import unittest
from calculator import Calculator
 
class TddInPythonExample(unittest.TestCase):
 
   def test_calculator_add_method_returns_correct_result(self):
      calc = Calculator()
      result = calc.add(2,2)
      self.assertEqual(4, result)
现在我们已经有了Calculator的定义了,让我们看看nosetest的提示:


所以,很显然,我们的添加的Calculator返回错误值,因为它不会做任何事情的时刻。很巧的是,nosetest能给出问题所在的那一行,这样我们可以确认,我们需要改变什么。让我们来修正Calculator方法,以保证我们的测试通过:
class Calculator(object):
 
   def add(self, x, y):
      return x+y

结果:


成功了!我们已经定义了add方法,它按预期工作。但是,我们还有更多的工作要做,以确保我们已经测试正确。
 如果有人要添加任何数字以外,会发生什么情况?实际上,Python会允许添加字符串和其他类型,但在我们的例子中,我们的计算器,它仅是允许添加数字。让我们添加另一个失败的测试,这种情况下,利用的assertRaises方法来测试:
import unittest
from calculator import Calculator
 
class TddInPythonExample(unittest.TestCase):
 
   def test_calculator_add_method_returns_correct_result(self):
      calc = Calculator()
      result = calc.add(2,2)
      self.assertEqual(4, result)
 
   def test_calculator_returns_error_message_if_both_args_not_numbers(self):
      self.assertRaises(ValueError, self.calc.add, 'two', 'three')

 结果:


现在又出现一个失败的例子,我们需要修改add函数来修正:

class Calculator(object):
 
   def add(self, x, y):
      number_types = (int, long, float, complex)
 
      if isinstance(x, number_types) and isinstance(y, number_types):
         return x+y
      else:
         raise ValueError

从上面的代码看来,我们已经添加一个小的修改:检查输入值的类型确保是函数需要的类型。为了更加全面的测试,我们还需要添加更多的测试用例:
import unittest
from calculator import Calculator
 
class TddInPythonExample(unittest.TestCase):
 
   def test_calculator_add_method_returns_correct_result(self):
      calc = Calculator()
      result = calc.add(2,2)
      self.assertEqual(4, result)
 
   def test_calculator_returns_error_message_if_both_args_not_numbers(self):
      self.assertRaises(ValueError, self.calc.add, 'two', 'three')
 
   def test_calculator_returns_error_message_if_x_arg_not_number(self):
      self.assertRaises(ValueError, self.calc.add, 'two', 3)
 
   def test_calculator_returns_error_message_if_x_arg_not_number(self):
      self.assertRaises(ValueError, self.calc.add, 2, 'three')
结果:


安装以及使用python的Nose

对于nose的安装可以采用:

"pip install nose"

安装后运行测试代码可以:

"nosetests example_unit_test.py"- to execute a singlefileof tests

Or:

"nosetests /path/to/tests"- to execute a suite of testsina folder

你需要遵循的唯一标准是“test_”开始每个测试的方法,以确保在nosetest可以找到您的测试!


一些有用的参数:


  • V:提供更详细的输出,包括正在执行的测试的名称
  • -S-nocapture:允许输出报表打印,这是正常获取和执行测试时隐藏。对调试有用。
  • - nologcapture:允许输出的日志信息
  • - rednose:一个可选的插件,可以下载,但提供了彩色输出的测试。