用Python进行行为驱动开发的入门教程


Posted in Python onApril 23, 2015

为驱动开发(Behavior-Driven Development,BDD)是一种卓越的开发模式。能帮助开发者养成日清日结的好习惯,从而避免甚至杜绝“最后一分钟”的情况出现,因此对提高代码质量是大有裨益的。其与Gherkin语法相结合的测试结构及设计形式,使得对团队的全部成员包括非技术人员都具有极好的易读性。

所有代码都必须进行测试,这意味着上线时把系统瑕疵降到最低甚至为零。这需要与完整的测试套件相配,从整体把控软件行为,使得检测与维护都能有序进行。这就是BDD的魅力所在,难道不心动吗?

什么是BDD?

BDD的概念和理论源自TDD(测试驱动开发),类似于TDD的理论要点是在编码前先写好测试。不同点是除了使用单元测试进行细粒度化测试,还使用接受测试(acceptance tests)贯穿程序始末。接下来我们会结合Lettuce测试框架进行讲解。

用Python进行行为驱动开发的入门教程

BDD过程可简单概括为:

  •     编写一个缺陷接受测试
  •     编写一个缺陷单元测试
  •     使单元测试通过
  •     重构
  •     使接受测试通过

在每个功能里,如有需要重复上述步骤。

敏捷开发中的BDD

在敏捷开发中,BDD更是如鱼得水。

如果项目的新功能和新需求每隔一、两个星期就发生变更,那么该团队需要配合进行快节奏的测试和编码工作。Python中的接受和单元测试可以帮助实现该目标。

接受测试为人熟知的是使用了一个英文格式的“特性”描述文件,内容是含有的测试以及个别测试。这样做的好处是使整个项目团队都参与其中,除了开发者,还有管理者与商业分析者等不参与实际测试过程的非技术成员。

特性文件的编写遵循全员可读的规则,使技术和非技术成员都能清楚理解与接收。如果只包含单元测试,那么有可能会导致需求分析不全面或不能达成共识。接受测试的最大优点是适用性强,不论项目规模大小都能运用自如。

Gherkin语法

通常会使用Gherkin来编写接受测试,Gherkin来自Cucumber框架,由Ruby语言所编写。Gherkin语法十分简单,在Lettuce Python中主要使用以下8点来进行特性和测试的定义:

  •     Given假设
  •     When时间
  •     Then下一步
  •     And与
  •     Feature特性:
  •     Background背景:
  •     Scenario Outline场合大纲:

安装

使用Python常用的pip install语句就可完成Lettuce包的安装:
 

$ pip install lettuce

$ lettuce /path/to/example.feature用于运行测试。可以每次只运行一个测试文件,或者是提交目录名来运行目录下的所有文件。

为了使测试的编写和使用更加容易,我们建议把nosetests也安装好:
 

$ pip install nose

特性文件

特性文件由英语写成,内容是测试所覆盖的程序范围。此外还包括测试的创建任务。换言之,你除了需要编写测试,还得规范自己就程序的方方面面编写出良好的文档。这样做的好处是使自己对代码上下都心中有数,明确下一步做什么。随着项目规模的扩大,文档的重要性会逐步显现;例如重新回顾某个功能或对某个调用API进行回溯等等。

接下来会结合TDD中的一个实例创建一个特性文件。该实例是一个由Python写成的简易计算器,同时会演示接受测试的基本写法。目录构成的建议是建立两个文件夹,一个是app,用于放置代码文件如calculator.py;另一个是tests,用于放置特性文件夹。

calculator.py:
 
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

tests/features目录下的特性文件calculator.feature
 

Feature: As a writer for NetTuts
 I wish to demonstrate
 How easy writing Acceptance Tests
 In Python really is.
 
 Background:
  Given I am using the calculator
 
 Scenario: Calculate 2 plus 2 on our calculator
  Given I input "2" add "2"
  Then I should see "4"

从该例子不难看出特性文件的描述是非常直截了当的,能够使全体成员都能看明白。

特性文件的三个要点:

  •     Feature block(特性区块):该处描述了测试组所涵盖的程序内容。这里不执行任何代码,但能使阅读者明白正要进行什么样的特性测试。
  •     Background block(背景区块):先于特性文件中每个场景(Scenario)区块执行。这类似于SetUp()方法用于进行创建代码的编写,例如进行条件和位置的编写。
  •     Scenario block(场景区块):这里用于定义测试。第一行用作文档再一次的描述,接着是测试的具体内容。以这样的风格编写测试难道不是很简单吗?

步骤(Steps)文件

除了特性文件,步骤文件也是必须的,这是“见证奇迹的时刻”。显然地,特性文件本身不会做出什么结果;它需要步骤文件依次地与Python执行代码一一映射才有最后的结果输出。这里应用的是正则表达式。

正则表达式?不会过于复杂吗?其实在BDD世界里,正则表达式常用于捕捉整个字符串或从某行抓取变量。所以熟能生巧。

正则表达式?在测试中使用不会太复杂吗?在Lettuce是不会的,反而是非常简单的。

以下是对应的步骤文件的编写:
 

from lettuce import *
from nose.tools import assert_equals
from app.calculator import Calculator
 
 
@step(u'I am using the calculator')
def select_calc(step):
  print ('Attempting to use calculator...')
  world.calc = Calculator()
 
 
@step(u'I input "([^"]*)" add "([^"]*)"')
def given_i_input_group1_add_group1(step, x, y):
  world.result = world.calc.add(int(x), int(y))
 
 
@step(u'I should see "([^"]+)"')
def result(step, expected_result):
  actual_result = world.result
  assert_equals(int(expected_result), actual_result)

文件首部分是标准的导入写法。例如对Calculator的访问和Lettuce工具的导入,还有nosetest包中assert_equals断定方法的导入。接下来,你就可以开始针对特性文件的每一行进行步骤定义。如前所述,正则表达式很多时候用于提取整个字符串,除了有时需要在某行对变量进行访问。

在这个例子中, 里的@step起到解码提取的作用;u字母的意思是以unicode编码方式进行表达式执行。接着是使用正则表达式对引用的内容进行匹配,这里是要进行相加的数字。

然后是对Python方法传入变量,变量名可任意定义,这里使用x和y作为calculator add方法的传入变量名。

此外需要介绍world变量的使用。world是一个全局容器,使得变量可以在同一场景的不同步骤中使用。否则,所有变量只对应于其所在方法可用。例如把add方法的运算结果存放于某个step,而在另一外一个step进行结果的断定。

特性的执行

特性文件和步骤文件都完成后,接下来可以运行测试来看看能否通过。内建测试运行机的Lettuce执行方式是很简单的,例如

lettuce test/features/calculator.feature:
 
$ lettuce tests/features/calculator.feature 
 
Feature: As a writer for NetTuts         # tests/features/calculator.feature:1
 I wish to demonstrate             # tests/features/calculator.feature:2
 How easy writing Acceptance Tests       # tests/features/calculator.feature:3
 In Python really is.              # tests/features/calculator.feature:4
 
 Background:
  Given I am using the calculator       # tests/features/steps.py:6
  Given I am using the calculator       # tests/features/steps.py:6
 
 Scenario: Calculate 2 plus 2 on our calculator # tests/features/calculator.feature:9
  Given I input "2" add "2"          # tests/features/steps.py:11
  Then I should see "4"            # tests/features/steps.py:16
 
1 feature (1 passed)
1 scenario (1 passed)
2 steps (2 passed)

Lettuce的输出是非常工整的,它清楚显示了哪行特性文件代码被执行了,然后对成功执行的行以高亮绿色显示。此外还显示了正在运行的特性文件以及行号,这对于测试失败时进行特性文件缺陷行的查找是很有帮助的。输出末尾是特性,场景,步骤的执行个数以及通过个数的结果汇总。本例中所有测试都通过了。但如果出现错误,Lettuce会如何处理呢?

首先得对calculator.py代码进行修改,把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

再次运行,看看Lettuce是如何对错误进行说明的:
 

$ lettuce tests/features/calculator.feature 
 
Feature: As a writer for NetTuts         # tests/features/calculator.feature:1
 I wish to demonstrate             # tests/features/calculator.feature:2
 How easy writing Acceptance Tests       # tests/features/calculator.feature:3
 In Python really is.              # tests/features/calculator.feature:4
 
 Background:
  Given I am using the calculator       # tests/features/steps.py:6
  Given I am using the calculator       # tests/features/steps.py:6
 
 Scenario: Calculate 2 plus 2 on our calculator # tests/features/calculator.feature:9
  Given I input "2" add "2"          # tests/features/steps.py:11
  Then I should see "4"            # tests/features/steps.py:16
  Traceback (most recent call last):
   File "/Users/user/.virtualenvs/bdd-in-python/lib/python2.7/site-packages/lettuce/core.py", line 144, in __call__
    ret = self.function(self.step, *args, **kw)
   File "/Users/user/Documents/Articles - NetTuts/BDD_in_Python/tests/features/steps.py", line 18, in result
    assert_equals(int(expected_result), actual_result)
   File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/case.py", line 515, in assertEqual
    assertion_func(first, second, msg=msg)
   File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/case.py", line 508, in _baseAssertEqual
    raise self.failureException(msg)
  AssertionError: 4 != 0
 
1 feature (0 passed)
1 scenario (0 passed)
2 steps (1 failed, 1 passed)
 
List of failed scenarios:
 Scenario: Calculate 2 plus 2 on our calculator # tests/features/calculator.feature:9

显然,实际结果0与预期结果4是不符的。Lettuce清楚显示了该问题,接下来就是调试排错直到通过的时间了。

其它工具
在Python中还提供了很多不同的工具来进行类似的测试,这些工具基本源自Cucumber。例如:

  •     Behave:这是一个Cucumber接口。文档配套齐备,保持更新,有不少的配套工具。
  •     Freshen:另一个Cucumber接口,配套网站有完整的教程和实例,安装方式都是简单的pip方式。

不论使用什么工具,只要对某个工具运用熟练了,其它的自然能融会贯通。对教程文档的熟读是成功的第一步。

优点

自信地进行代码重构

使用一个完整测试套件的优点是显而易见的。找到一个强大的测试套件,会让代码重构工作事半功倍,信心满满。

随着项目规模的不断扩大,如果缺乏有效的工具,这不啻会使回溯和重构工作困难重重。如果有一套完整的接受测试来与每个特性一一对应,那么将能使变更工作有序不紊地进行,不会对现有功能模块造成破坏。

全员都能参与其中的接受测试,将能极大地提升团队战斗力,一开始就朝着同一目标前进。程序员可把精力用在精确的目标上,避免需求范围的失控;测试员可就特性文件进行一一检阅,把测试环节做到极致。最后形成良性循环,使得程序的每个特性都完美交付。

综述

结合上述过程和工具,在过往工作过的团队中我们都曾取得不错的成绩。BDD开发方式可使整个团队保持专注,保持自信,保持活力,并使潜在错误降到最低。

Python 相关文章推荐
Python 条件判断的缩写方法
Sep 06 Python
Python采用socket模拟TCP通讯的实现方法
Nov 19 Python
Python中处理字符串之isalpha()方法的使用
May 18 Python
Python的Django中将文件上传至七牛云存储的代码分享
Jun 03 Python
Python中%r和%s的详解及区别
Mar 16 Python
python记录程序运行时间的三种方法
Jul 14 Python
python中多个装饰器的执行顺序详解
Oct 08 Python
python numpy数组的索引和切片的操作方法
Oct 20 Python
Django实现跨域的2种方法
Jul 31 Python
Python 日期区间处理 (本周本月上周上月...)
Aug 08 Python
Manjaro、pip、conda更换国内源的方法
Nov 17 Python
python xlwt模块的使用解析
Apr 13 Python
python正常时间和unix时间戳相互转换的方法
Apr 23 #Python
python执行等待程序直到第二天零点的方法
Apr 23 #Python
在Python中测试访问同一数据的竞争条件的方法
Apr 23 #Python
python实现在每个独立进程中运行一个函数的方法
Apr 23 #Python
python输出指定月份日历的方法
Apr 23 #Python
python打开文件并获取文件相关属性的方法
Apr 23 #Python
Python实现计算文件夹下.h和.cpp文件的总行数
Apr 23 #Python
You might like
我的群发邮件程序
2006/10/09 PHP
使用 MySQL 开始 PHP 会话
2006/12/21 PHP
php GeoIP的使用教程
2011/03/09 PHP
php启动时候提示PHP startup的解决方法
2013/05/07 PHP
ubuntu10.04配置 nginx+php-fpm模式的详解
2013/06/03 PHP
ThinkPHP模版引擎之变量输出详解
2014/12/05 PHP
PHP判断是否为空的几个函数对比
2015/04/21 PHP
PHP+JS三级菜单联动菜单实现方法
2016/02/24 PHP
CI框架实现创建自定义类库的方法
2018/12/25 PHP
php生成静态页面并实现预览功能
2019/06/27 PHP
在textarea中显示html页面的javascript代码
2007/04/20 Javascript
jQuery 使用手册(五)
2009/09/23 Javascript
解决3.01版的jquery.form.js中文乱码问题的解决方法
2012/03/08 Javascript
10个基于浏览器的JavaScript调试工具分享
2013/02/07 Javascript
JS回调函数的应用简单实例
2014/09/17 Javascript
用Move.js配合创建CSS3动画的入门指引
2015/07/22 Javascript
jQuery获取复选框被选中数量及判断选择值的方法详解
2016/05/25 Javascript
webpack v4 从dev到prd的方法
2018/04/02 Javascript
layui table 表格模板按钮的实例代码
2019/09/21 Javascript
javascript实现前端分页功能
2020/11/26 Javascript
[01:36:17]DOTA2-DPC中国联赛 正赛 Ehome vs iG BO3 第一场 1月31日
2021/03/11 DOTA
python提取字典key列表的方法
2015/07/11 Python
Python实现网络端口转发和重定向的方法
2016/09/19 Python
用 Python 爬了爬自己的微信朋友(实例讲解)
2017/08/25 Python
Python使用pyserial进行串口通信的实例
2019/07/02 Python
Django之choices选项和富文本编辑器的使用详解
2020/04/01 Python
加拿大女装网上购物:Reitmans
2016/10/20 全球购物
施华洛世奇巴西官网:SWAROVSKI巴西
2019/12/03 全球购物
J2SDK1.5与J2SDK5.0有什么区别
2012/09/19 面试题
西安交大自主招生自荐信
2014/01/27 职场文书
幼儿园安全检查制度
2014/01/30 职场文书
供货协议书范本
2014/04/22 职场文书
导师就业推荐信范文
2014/05/22 职场文书
个人承诺书格式
2014/06/03 职场文书
远程教育学习心得体会
2016/01/23 职场文书
2019年“红色之旅”心得体会1000字(3篇)
2019/09/27 职场文书