测试Flask应用

没有经过测试的东西都是不完整的

这一箴言的起源已经不可考了,尽管他不是完全正确的,但是仍然离真理 不远。 没有测试过的应用将会使得提高现有代码质量很困难,二不测试应用 程序的开发者,会显得特别多疑。 如果一个应用拥有自动化测试,那么您就 可以安全的修改然后立刻知道是否有错误。

Flask提供了一种方法用于测试您的应用,那就是将Werkzeug测试Client暴露出来,并且为你处理上下文局部变量。 然后您就可以将自己最喜欢的测试解决方案应用于其上了。 在这片文档中,我们将会使用Python自带的unittest包。

应用

首先,我们需要一个应用来测试,我们将会使用 教程 这里的应用 来演示。 如果你还没有这个应用,请从示例中获取源码。

测试框架

为了测试这个应用,我们添加第二个模块(flaskr_tests.py), 并在其中创建一个单元测试框架:

import os
import flaskr
import unittest
import tempfile

class FlaskrTestCase(unittest.TestCase):

    def setUp(self):
        self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
        flaskr.app.config['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配置标志被激活。它所做的是禁用处理请求时的错误捕捉,这样你在进行对应用发出请求的测试时获得更好的错误报告。

这个测试客户端将会给我们一个通向应用的简单接口。我们可以触发对向应用发送请求的测试,并且此客户端也会帮我们记录 Cookie 的 动态。

因为SQLite3是基于文件系统的,我们可以很容易的使用临时文件模块来创建一个临时的数据库并初始化它。函数mkstemp()实际上完成了两件事情:它返回了一个底层的文件指针以及一个随机的文件名,后者我们用作数据库的名字。我们只需要将db_fd变量保存起来,这样就可以使用os.close()方法来关闭这个文件。

如果我们运行这套测试,我们应该会得到如下的输出:

$ python flaskr_tests.py

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

虽然现在还未进行任何实际的测试,我们已经可以知道我们的flaskr应用在语法是合法的,否则import将会由于异常而失败。

第一个测试

是进行第一个应用功能的测试的时候了。 让我们检查如果我们访问根路径(/),它将显示“No entries here so far”。 为此,我们添加了一个新的测试函数到我们的类当中, 如下面的代码所示:

class FlaskrTestCase(unittest.TestCase):

    def setUp(self):
        self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
        self.app = flaskr.app.test_client()
        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

登入和登出

我们应用的大部分功能只允许具有管理员资格的用户访问,所以我们需要一种方法来帮助我们的测试客户端登入和登出。为此,我们向登入和登出页面发送一些请求,这些请求都携带了表单数据(用户名和密码)。因为登入和登出页面都会重定向,我们将客户端设置为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

测试添加消息

我们同时应该测试消息的添加功能是否正常。添加一个新的测试方法,如下:

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'&lt;Hello&gt;' 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,在这个例子的源代码里包含一套更长的测试。

其他测试技巧

除了如上文演示的使用测试客户端完成测试的方法,还有一个test_request_context()方法可以配合with语句用于激活一个临时的请求上下文。 通过 它,您可以访问 requestgsession 类的对象,就像在视图中一样。 这里有一个完整的例子示范了这种用法:

import flask

app = flask.Flask(__name__)

with app.test_request_context('/?name=Peter'):
    assert flask.request.path == '/'
    assert flask.request.args['name'] == 'Peter'

所有其他的和上下文绑定的对象都可以使用同样的方法访问。

如果您希望测试应用在不同配置的情况下的表现,这里似乎没有一个 很好的方法,考虑使用应用的工厂函数(参考 应用程序的工厂函数)

注意,尽管你在使用一个测试用的请求环境,函数 before_request() 以及 after_request() 都不会自动运行。 然而,teardown_request()函数在测试请求的上下文离开with块的时候会执行。如果你希望before_request()函数仍然执行,你需要自己调用preprocess_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)
    ...

这通常不是很有效,因为这时您可以直接转向使用 测试客户端。

伪造资源和上下文

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)

保存上下文

0.4 新版功能.

有时,激发一个通常的请求,但是将当前的上下文 保存更长的时间,以便于附加的内省发生是很有用的。 在Flask 0.4 中,通过test_client()函数和with块的使用可以实现:

app = flask.Flask(__name__)

with app.test_client() as c:
    rv = c.get('/?tequila=42')
    assert request.args['tequila'] == '42'

如果你仅仅使用test_client()方法,而不使用with代码块, assert会失败,因为request不再可访问(因为你试图在非真正请求中时候访问它)。

访问和修改 Sessions

0.8 新版功能.

有时,在测试客户端里访问和修改 Sesstions 可能会非常有用。 通常有两种方法实现这种需求。 如果您仅仅希望确保一个 Session 拥有某个特定的键,且此键的值是某个特定的值,那么您可以只 保存起上下文,并且访问 flask.session:

with app.test_client() as c:
    rv = c.get('/')
    assert flask.session['foo'] == 42

但是这样做并不能使你修改Session或在请求发出之前访问Session。 从 Flask 0.8 开始,我们提供一个叫做 “Session 事务” 的东西用于 模拟适当的调用,从而在测试客户端的上下文中打开一个 Session,并 用于修改。 在事务结束时,Session将被保存。它与使用的Session后端无关:

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 代理。而这个对象本身提供了同样的接口。