英文原文地址:http://flask.pocoo.org/docs/0.12/testing/
若有翻译错误或者不尽人意之处,请指出,谢谢~
(懒得改了,想要使用pytest的朋友,请前往http://flask.pocoo.org/docs/1.0/testing/)
一些未经测试的东西被破坏了。
这句话的出处不祥,虽然它不是完全正确的,但是也离真相不远了。未经测试的应用程序对于改进现有代码是非常困难的,而未经测试的应用程序的开发人员往往也是偏执的。如果一个应用程序拥有自动测试,那么你可以安全地就行修改并且你可以马上知道是否有东西导致程序中断。
Flask提供了一种测试你的应用程序的方法:公开Werkzeug测试客户端,并且为你处理本地上下文。你可以随后在你喜欢的测试环境中使用这个。在这里,我们将使用Python已经预先安装好的unittest包。
1. 应用程序
首先,我们需要一个应用程序进行测试;这里我们将使用第4章完成的flaskr,如果你直接跳过了之前的章节,请从这里获取源码。
2. 测试框架
为了测试这个应用程序,我们添加另一个模块(flaskr_tests.py)并且创建一个unittest框架:
import os
from flaskr import flaskr
import unittest
import tempfile
class FlaskrTestCase(unittest.TestCase):
def setUp(self):
self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
flaskr.app.testing = True
self.app = flaskr.app.test_client()
with flaskr.app.app_context():
flaskr.init_db()
def tearDown(self):
os.close(self.db_fd)
os.unlink(flaskr.app.config['DATABASE'])
if __name__ == '__main__':
unittest.main()
setUp()函数创建了一个新的测试客户端,并且初始化了一个新的数据库。这个方法是在每个单独的测试方法运行之前调用的。为了在测试完成后删除数据库,我们在tearDown()函数中关闭这个文件链接并且从文件系统中删除这个文件。此外,在设置过程中,TESTING配置标记被激活。它所做的是在请求处理期间禁用错误捕获,以便在对应用程序执行测试请求时获得更好的错误报告。
这个测试客户端会为我们提供一个简单的应用程序接口。我们能触发应用程序的测试请求,并且这个客户端也能为我们追踪cookies。
因为SQLite3是基于文件系统的,所以我们能够很容易使用tempfile模块来创建一个临时数据库并且初始化它。mkstemp()方法为我们做了两件事:它返回了一个低等级的文件句柄以及一个随机文件名,这个随机文件名就是我们随后使用的数据库名。我们仅仅需要保留db_fd,这样我们就可以使用os.close()方法来关闭文件。
如果我们运行这个测试集,我们可以看到如下输出:
$ python flaskr_tests.py
----------------------------------------------------------------------
Ran 0 tests in 0.000s
OK
即使它没有运行任何实际的测试代码,我们也已经知道我们的flaskr应用程序在语法上是正确的,否则就会显示我们死在什么异常上了。
3. 第一个测试
现在我们将要开始实现应用程序的测试代码编写。首先,我们检查一下,如果访问的是应用程序的根目录(/),应用程序将显示“No entries here so far”。为了实现这个功能,我们将添加新的测试方法:
class FlaskrTestCase(unittest.TestCase):
def setUp(self):
self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
flaskr.app.testing = True
self.app = flaskr.app.test_client()
with flaskr.app.app_context():
flaskr.init_db()
def tearDown(self):
os.close(self.db_fd)
os.unlink(flaskr.app.config['DATABASE'])
def test_empty_db(self):
rv = self.app.get('/')
assert b'No entries here so far' in rv.data
需要注意的是,我们的测试方法都是以test开头的,这允许unittest自动标记这种方法作为一个测试方法来运行。
通过使用self.app.get,我们可以发送一个伴随着指定地址的HTTP的GET请求到应用程序。其返回值是一个response_class对象。我们现在可以使用data属性来检查来自应用程序的返回值(作为字符串)。在这种情形下,我们确保‘No entries here so far’是输出值的一部分。
再次运行测试集,你会发现一条通过测试的信息:
$ python flaskr_tests.py
.
----------------------------------------------------------------------
Ran 1 test in 0.034s
OK
4. 登录和登出
我们程序的大部分功能仅供管理员使用,因此我们需要一种方式来记录我们的测试客户端登录和登出应用程序。为了实现这个目标,我们发送一些请求到登录和登出页面,并使用表单数据(用户名和密码)。并且因为登录和登出页面会重定向,我们要告诉客户端需要follow_redirects。
添加下面两个函数到你的FlaskrTestCase类中:
def login(self, username, password):
return self.app.post('/login', data=dict(
username=username,
password=password,
), follow_redirects=True)
def logout(self):
return self.app.get('/logout', follow_redirects=True)
现在我们可以很容易测试登录和登出的功能,如果功能失败的话,一般是认证信息非法。添加这两个新的测试到类中:
def test_login_logout(self):
rv = self.login('admin', 'default')
assert b'You were logged in' in rv.data
rv = self.logout()
assert b'You were logged out' in rv.data
rv = self.login('adminx', 'default')
assert b'Invalid username' in rv.data
rv = self.login('admin', 'defaultx')
assert b'Invalid password' in rv.data
5. 测试添加信息
我们也应该测试添加信息的功能。添加一个新的测试方法如下:
def test_messages(self):
self.login('admin', 'default')
rv = self.app.post('/add', data=dict(
title='<Hello>',
text='<strong>HTML</strong> allowed here'
), follow_redirects=True)
assert b'No entries here so far' not in rv.data
assert b'<Hello>' in rv.data
assert b'<strong>HTML</strong> allowed here' in rv.data
这里我们将检查HTML是否允许出现在文本中而不是标题中,这是预期的行为。
运行测试集,得到三个测试通过的信息:
$ python flaskr_tests.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.332s
OK
对于带有头和状态码的更为复杂的测试,请前往MiniTwit Example获取源码,其包含了一个很大的测试集。
6. 其他测试技巧
除了上面使用测试客户端外,还可以使用test_request_context()函数,它可以结合with语句来激活一个临时的请求上下文。这种方式下,你可以像在视图函数中一样访问request,g和session对象。这里有一个示例来展示上述内容:
import flask
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
assert flask.request.path == '/'
assert flask.request.args['name'] == 'Peter'
所有上下文绑定的对象都可以以相同的方式被使用。
如果你想要在不同的配置下测试你的应用程序的情况,并且你目前没有什么好的办法来实现,那么你可以考虑应用程序工厂(参见Application Factories)。
然后请注意,如果你正在使用一个测试请求上下文,那么before_request()和after_request()方法将不会被自动调用。然而,当测试请求上下文离开with块时,teardown_request()方法却能被执行。如果你同样想要before_request()方法被调用,你需要主动调用preprogress_request()方法:
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
app.preprocess_request()
...
这对于数据库连接或类似的东西来说是必要的,这取决于你的程序是如何设计的。
如果你想要调用after_request()方法,你需要调用process_response()方法,但是这个方法需要你传递一个响应对象:
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
resp = Response('...')
resp = app.process_response(resp)
...
这通常是没什么卵用的,因为你可以直接使用测试客户端的方式来实现测试。
7. 伪资源和上下文
(在0.10版本新增)
一种很常见的模式是在应用程序上下文或者flask.g对象中存储用户验证信息和数据库连接。一般模式是在第一次使用它的时候存储这个对象,然后在销毁的时候移除这个对象。想象一下获取当前用户的代码:
def get_user():
user = getattr(g, 'user', None)
if user is None:
user = fetch_current_user_from_database()
g.user = user
return user
对于一个测是来说,最好在不用更改某些代码的情况下从外部覆盖这个用户对象。这可以通过连接flask.appcontext_pushed信号来完成:
from contextlib import contextmanager
from flask import appcontext_pushed, g
@contextmanager
def user_set(app, user):
def handler(sender, **kwargs):
g.user = user
with appcontext_pushed.connected_to(handler, app):
yield
下面是如何使用它:
from flask import json, jsonify
@app.route('/users/me')
def users_me():
return jsonify(username=g.user.username)
with user_set(app, my_user):
with app.test_client() as c:
resp = c.get('/users/me')
data = json.loads(resp.data)
self.assert_equal(data['username'], my_user.username)
8. 保持上下文的环绕
(在0.4版本新增)
有时候触发一个常规请求是很有用的,但仍然将上下文保持更长的时间,以便发生额外的内省。在Flask0.4,可以在with块中使用test_client()方法来实现:
app = flask.Flask(__name__)
with app.test_client() as c:
rv = c.get('/?tequila=42')
assert request.args['tequila'] == '42'
如果你不是在with块中使用test_client()方法,assert会失败并伴随一个错误,因为请求并不是可用的了(因为你正在尝试在实际请求外使用它)。
9. 访问和修改会话
(在0.8版本新增)
有时候,在测试客户端访问或修改会话是非常有用的。一般而言,这里有两种方法可以实现它。如果你仅仅想要确定在会话中确定的键设置为了确定的值,你仅需要保持上下文并且访问flask.session即可:
with app.test_client() as c:
rv = c.get('/')
assert flask.session['foo'] == 42
然而,这并不能在请求触发之前访问或者修改会话。从Flask0.8开始提供了一个叫做“会话事务”的东西,它模拟了在测试客户端的上下文中打开一个会话并且可以修改它。在事务的最后,这个会话被保存了。它是独立于会话后端而工作的:
with app.test_client() as c:
with c.session_transaction() as sess:
sess['a_key'] = 'a value'
# once this is reached the session was stored
注意,在这种情况下,你必须使用sess对象,而不是
flask.session代理。而sess对象将提供与
flask.session同样的接口。