本文通过一个使用flask框架开发的web项目,介绍了python中行为驱动开发模式,包括BDD的语法,结构和目标。
简介
行为驱动开发模式,是指先对项目的行为进行描述,然后增加相应的测试和代码,使这个行为的测试通过。通过对不同场景中行为的清晰描述,你可以保证在项目最后交付时,能够满足产品的需求。BDD模式能够让能一块一块的编写你的项目,并且提供了你整个系统的living文档(living documentation,不知道怎么翻译),这个文档是你在保持测试通过是所自然维护的。
前提
在开始本教程之前,请确保你的系统已安装以下环境:
* Python2.7X
* Lettuce
* flask
* Nosetests
* 对于RESET 原则的基本理解
设置项目的基本结构
在本教程中,你将会构建一个简单的resetful项目,用来处理用户数据的存储和检索。首先,在你的系统中创建如下所示的目录结构,文件均为空,内容以后再添加:
文件的描述如下:
* __init__.py: 使当前目录作为一个python包的存在。
* steps.py:将会被.feature文件执行的python代码
* user.feature:描述项目中用户行为的测试文件。
* application.py:Flask项目的创建入口以及服务开始的地方。
* views.py:处理视图注册的代码,以及定义了视图上对各种http请求的响应。
创建flask架构的项目
为了完成本教程,你需要使用flask框架,构建一个简单的web项目。打开application.py文件,添加以下代码:
from flask import Flask
app = Flask(__name__)
if __name__ == "__main__":
app.run()
如果你已经安装好了必须的环境,打开系统命令行,输入如下命令:
python app/appliction.py
如果能看到以下输出,说明你的flask项目是正常运行的,然后你可以继续这个教程了。
$ python app/application.py
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
第一个BDD测试
我们将会依照BDD的开发模式,首先写测试,用来描述了我们想要在我们的应用程序中开发的初始功能。一旦测试写完,并且运行失败了,我们将会接着写项目代码,使测试通过。
写feature文件
编辑user.feature,添加如下代码:
Scenario: Retrieve a customers details
Given some users are in the system
When I retrieve the customer 'david01'
Then I should get a '200' response
And the following user details are returned:
| name |
| David Sale |
你会发现测试中的关键字使用了Gherkin(e.g. Given, When, Then, And)标准。
另一个重要的值得注意的是测试的风格和它如何读取。你想使你的场景尽可能容易阅读和重复使用,让任何人了解测试正在做什么,测试的功能和你期望它的行为。你应该尽可能的重用你的步骤,这样能保证你需要写的新代码的数量尽可能少,以及维持单元测试的高一致性。我们将在后面的教程中介绍一些可重用步骤,如将步骤中参数作为值。
如果你的系统中以及安装lettuce,现在就可以通过系统的命令行执行user.feature文件了。
lettuce test/features
你将会看到如下输出:
$ lettuce test/features/
Feature: Handle storing, retrieving and deleting customer details # test/features/user.feature:1
Scenario: Retrieve a customers details # test/features/user.feature:3
Given some users are in the system # test/features/user.feature:4
When I retrieve the customer 'david01' # test/features/user.feature:5
Then I should get a '200' response # test/features/user.feature:6
And the following user details are returned: # test/features/user.feature:7
| name |
| David Sale |
1 feature (0 passed)
1 scenario (0 passed)
4 steps (4 undefined, 0 passed)
You can implement step definitions for undefined steps with these snippets:
[ example snippets removed for readability ]
你会明显的发现测试没有通过当我们还没有写任何可被.feature文件所执行的代码时。这个被执行的代码被定义为steps。
定义steps
打开steps.py文件,添加如下代码:
from lettuce import step, world, before
from nose.tools import assert_equals
from app.application import app
from app.views import USERS
@before.all
def before_all():
world.app = app.test_client()
from app.views import USERS
@step(u'Given some users are in the system')
def given_some_users_are_in_the_system(step):
USERS.update({'david01': {'name': 'David Sale'}})
@step(u'When I retrieve the customer \'(.*)\'')
def when_i_retrieve_the_customer_group1(step, username):
world.response = world.app.get('/user/{}'.format(username))
@step(u'Then I should get a \'(.*)\' response')
def then_i_should_get_a_group1_response_group2(step, expected_status_code):
assert_equals(world.response.status_code, int(expected_status_code))
@step(u'And the following user details are returned:')
def and_the_following_user_details(step):
assert_equals(step.hashes, [json.loads(world.response.data)])
@before.all这步,在lettuce中作为hook,即钩子的存在,是指在场景开始测试之前,所需要执行的代码。
接下来,定义了“Given some users are in the system”,当系统中存在一些用户时的操作。我们需要在view.py中添加如下代码:
USERS = {}
“When I retrieve the customer \’(.*)\’”这句,使用了正则表达式,匹配断言中的参数,并执行相应的操作。
“Then I should get a \’(.*)\’ response”这个步骤中,调用了nose.tools 的assert_equals函数,来判断上一步操作之后的返回值,与期望值是否相同。最后一句“And the following user details are returned:”原理类似。
执行场景
接下来,再运行一次测试:
lettuce test/features
可以看到如下输出:
$ lettuce test/features/
Feature: Handle storing, retrieving and deleting customer details # test/features/user.feature:1
Scenario: Retrieve a customers details # test/features/user.feature:3
Given some users are in the system # test/features/steps.py:17
When I retrieve the customer 'david01' # test/features/steps.py:22
Then I should get a '200' response # test/features/steps.py:27
Traceback (most recent call last):
[ SNIPPET REMOVED FOR READABILITY ]
raise self.failureException(msg)
AssertionError: 404 != 200
And the following user details are returned: # test/features/steps.py:32
| name |
| David Sale |
1 feature (0 passed)
1 scenario (0 passed)
4 steps (1 failed, 1 skipped, 2 passed)
List of failed scenarios:
Scenario: Retrieve a customers details # test/features/user.feature:3
正如你所看到的那样,我们的项目返回“404 not found”的响应。那是因为我们还没有定义URL“/user/”的操作。在view.py中加入如下代码:
GET = 'GET'
@app.route("/user/<username>", methods=[GET])
def access_users(username):
if request.method == GET:
user_details = USERS.get(username)
if user_details:
return jsonify(user_details)
else:
return Response(status=404)
这段代码首先为你的flask项目注册了一个URL。然后定义了处理这个URL的方法。
再一次执行测试,你将会看到测试通过:
$ lettuce test/features/
Feature: Handle storing, retrieving and deleting customer details # test/features/user.feature:1
Scenario: Retrieve a customers details # test/features/user.feature:3
Given some users are in the system # test/features/steps.py:17
When I retrieve the customer 'david01' # test/features/steps.py:22
Then I should get a '200' response # test/features/steps.py:27
And the following user details are returned: # test/features/steps.py:32
| name |
| David Sale |
1 feature (1 passed)
1 scenario (1 passed)
4 steps (4 passed)
额外的任务
如果你喜欢这个教程,为什么不通过BDD的方法扩展你的flask项目,以满足如下要求:
* 支持POST操作在USERS中添加新的用户
* 支持PUT操作更新USERS中用户的信息
* 支持DELETE操作删除USERS中的用户
总结
行为驱动的开发是一个很好的过程,无论你是一个单独的开发人员工作在一个小项目,或是大型企业应用程序的开发人员。该过程确保你的代码符合前面设置的要求,在开始开发要交付的功能之前提供了正式的短暂的思考。BDD还有额外的好处,它可以自然的为你的代码提供“活着”的文档( “living” documentation),并且保持测试都是最新的,当更新新的功能时。
通过学习本教程,您希望获得编写这种样式的行为测试所需的核心技能,执行测试并传递使它们通过的代码。