未經(jīng)測試的小貓,肯定不是一只好貓。
這句話的出處不詳(譯者注:這句是譯者獻(xiàn)給小貓的),也不一定完全正確,但是基本上是正確的。未經(jīng)測試的應(yīng)用難于改進(jìn)現(xiàn)有的代碼,因此其開發(fā)者會(huì)越改進(jìn)越抓狂。反之, 經(jīng)過自動(dòng)測試的代碼可以安全的改進(jìn),并且如果可以測試過程中立即發(fā)現(xiàn)錯(cuò)誤。
Flask 提供的測試渠道是公開 Werkzeug 的 Client ,為你 處理本地環(huán)境。你可以結(jié)合這個(gè)渠道使用你喜歡的測試工具。本文使用的測試工具是隨著 Python 一起安裝好的 unittest 包。
首先,我們需要一個(gè)用來測試的應(yīng)用。我們將使用教程中的應(yīng)用。如果你還沒有這個(gè)應(yīng)用,可以下載示例代碼 。
為了測試應(yīng)用,我們添加了一個(gè)新的模塊 (flaskr_tests.py) 并創(chuàng)建了如下測試骨架:
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()
flaskr.init_db()
def tearDown(self):
os.close(self.db_fd)
os.unlink(flaskr.app.config['DATABASE'])
if __name__ == '__main__':
unittest.main()
setUp() 方法中會(huì)創(chuàng)建一個(gè)新的測試客戶端并初始化一個(gè)新的數(shù)據(jù)庫。在每個(gè)獨(dú)立的測試函數(shù)運(yùn)行前都會(huì)調(diào)用這個(gè)方法。 tearDown() 方法的功能是在測試結(jié)束后關(guān)閉文件,并在文件系統(tǒng)中刪除數(shù)據(jù)庫文件。另外在設(shè)置中 TESTING 標(biāo)志開啟的,這意味著在請求時(shí)關(guān)閉 錯(cuò)誤捕捉,以便于在執(zhí)行測試請求時(shí)得到更好的錯(cuò)誤報(bào)告。
測試客戶端會(huì)給我們提供一個(gè)簡單的應(yīng)用接口。我們可以通過這個(gè)接口向應(yīng)用發(fā)送測試請求。客戶端還可以追蹤 cookies 。
因?yàn)?SQLite3 是基于文件系統(tǒng)的,所以我們可以方便地使用臨時(shí)文件模塊來創(chuàng)建一個(gè)臨時(shí)數(shù)據(jù)庫并初始化它。 mkstemp() 函數(shù)返回兩個(gè)東西:一個(gè)低級(jí)別的文件 句柄和一個(gè)隨機(jī)文件名。這個(gè)文件名后面將作為我們的數(shù)據(jù)庫名稱。我們必須把句柄保存到 db_fd 中,以便于以后用 os.close() 函數(shù)來關(guān)閉文件。
如果現(xiàn)在進(jìn)行測試,那么會(huì)輸出以下內(nèi)容:
$ python flaskr_tests.py
----------------------------------------------------------------------
Ran 0 tests in 0.000s
OK
雖然沒有運(yùn)行任何實(shí)際測試,但是已經(jīng)可以知道我們的 flaskr 應(yīng)用沒有語法錯(cuò)誤。 否則在導(dǎo)入時(shí)會(huì)引發(fā)異常并中斷運(yùn)行。
現(xiàn)在開始測試應(yīng)用的功能。當(dāng)我們訪問應(yīng)用的根 URL ( / )時(shí)應(yīng)該顯示 “ No entries here so far ”。我們新增了一個(gè)新的測試方法來測試這個(gè)功能:
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 'No entries here so far' in rv.data
注意,我們的調(diào)試函數(shù)都是以 test 開頭的。這樣 unittest 就會(huì)自動(dòng)識(shí)別這些是用于測試的函數(shù)并運(yùn)行它們。
通過使用 self.app.get ,可以向應(yīng)用的指定 URL 發(fā)送 HTTP GET 請求,其返回的是一個(gè) ~flask.Flask.response_class 對象。我們可以使用 data 屬性來檢查應(yīng)用的返回值(字符串類型)。在本例中,我們檢查輸出是否包含 'No entries here so far' 。
再次運(yùn)行測試,會(huì)看到通過了一個(gè)測試:
$ python flaskr_tests.py
.
----------------------------------------------------------------------
Ran 1 test in 0.034s
OK
我們應(yīng)用的主要功能必須登錄以后才能使用,因此必須測試應(yīng)用的登錄和注銷。測試的 方法是使用規(guī)定的數(shù)據(jù)(用戶名和密碼)向應(yīng)用發(fā)出登錄和注銷的請求。因?yàn)榈卿浐妥N后會(huì)重定向到別的頁面,因此必須告訴客戶端使用 follow_redirects 追蹤重定向。
在 FlaskrTestCase 類中添加以下兩個(gè)方法:
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)
現(xiàn)在可以方便地測試登錄成功、登錄失敗和注銷功能了。下面為新增的測試代碼:
def test_login_logout(self):
rv = self.login('admin', 'default')
assert 'You were logged in' in rv.data
rv = self.logout()
assert 'You were logged out' in rv.data
rv = self.login('adminx', 'default')
assert 'Invalid username' in rv.data
rv = self.login('admin', 'defaultx')
assert '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 'No entries here so far' not in rv.data
assert '<Hello>' in rv.data
assert '<strong>HTML</strong> allowed here' in rv.data
這里我們檢查了博客內(nèi)容中允許使用 HTML ,但標(biāo)題不可以。應(yīng)用設(shè)計(jì)思路就是這樣的。
運(yùn)行測試,現(xiàn)在通過了三個(gè)測試:
$ python flaskr_tests.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.332s
OK
關(guān)于更復(fù)雜的 HTTP 頭部和狀態(tài)碼測試參見 MiniTwit 示例 。這個(gè)示例的源代碼中 包含了更大的測試套件。
除了使用上述測試客戶端外,還可以在 with 語句中使用 test_request_context() 方法來臨時(shí)激活一個(gè)請求環(huán)境。在這個(gè) 環(huán)境中可以像以視圖函數(shù)中一樣操作 request 、g 和 session 對象。示例:
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
assert flask.request.path == '/'
assert flask.request.args['name'] == 'Peter'
其他與環(huán)境綁定的對象也可以這樣使用。
如果你必須使用不同的配置來測試應(yīng)用,而且沒有什么好的測試方法,那么可以考慮使用應(yīng)用工廠(參見應(yīng)用工廠 )。
注意,在測試請求環(huán)境中 before_request() 函數(shù)和 after_request() 函數(shù)不會(huì)被自動(dòng)調(diào)用。但是當(dāng)調(diào)試請求環(huán)境離開 with 塊時(shí)會(huì)執(zhí)行 teardown_request() 函數(shù)。如果需要 before_request() 函數(shù)和正常情況下一樣被調(diào)用,那么你必須調(diào)用 preprocess_request()
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
app.preprocess_request()
...
在這函數(shù)中可以打開數(shù)據(jù)庫連接或者根據(jù)應(yīng)用需要打開其他類似東西。
如果想調(diào)用 after_request() 函數(shù),那么必須調(diào)用 process_response() ,并把響應(yīng)對象傳遞給它:
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
resp = Response('...')
resp = app.process_response(resp)
...
這個(gè)例子中的情況基本沒有用處,因?yàn)樵谶@種情況下可以直接開始使用測試客戶端。
New in version 0.10.
通常情況下,我們會(huì)把用戶認(rèn)證信息和數(shù)據(jù)庫連接儲(chǔ)存到應(yīng)用環(huán)境或者 flask.g 對象中,并在第一次使用前準(zhǔn)備好,然后在斷開時(shí)刪除。假設(shè)應(yīng)用中得到當(dāng)前用戶的代碼如下:
def get_user():
user = getattr(g, 'user', None)
if user is None:
user = fetch_current_user_from_database()
g.user = user
return user
在測試時(shí)可以很很方便地重載用戶而不用改動(dòng)代碼。可以先象下面這樣鉤接 flask.appcontext_pushed 信號(hào):
from contextlib import contextmanager
from flask import appcontext_pushed
@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)
New in version 0.4.
有時(shí)候這種情形是有用的:觸發(fā)一個(gè)常規(guī)請求,但是保持環(huán)境以便于做一點(diǎn)額外的事情。 在 Flask 0.4 之后可以在 with 語句中使用 test_client() 來 實(shí)現(xiàn):
app = flask.Flask(__name__)
with app.test_client() as c:
rv = c.get('/?tequila=42')
assert request.args['tequila'] == '42'
如果你在沒有 with 的情況下使用 test_client() ,那么 assert 會(huì)出錯(cuò)失敗。因?yàn)闊o法在請求之外訪問 request 。
New in version 0.8.
有時(shí)候在測試客戶端中訪問和修改會(huì)話是非常有用的。通常有兩方法。如果你想測試會(huì)話中 的鍵和值是否正確,你可以使用 flask.session:
with app.test_client() as c:
rv = c.get('/')
assert flask.session['foo'] == 42
但是這個(gè)方法無法修改會(huì)話或在請求發(fā)出前訪問會(huì)話。自 Flask 0.8 開始,我們提供了 “會(huì)話處理”,用打開測試環(huán)境中會(huì)話和修改會(huì)話,最后保存會(huì)話。處理后的會(huì)話獨(dú)立于 后端實(shí)際使用的會(huì)話:
with app.test_client() as c:
with c.session_transaction() as sess:
sess['a_key'] = 'a value'
# 運(yùn)行到這里時(shí),會(huì)話已被保存
注意在這種情況下必須使用 sess 對象來代替 flask.session 代理。 sess 對象本身可以提供相同的接口。
? Copyright 2013, Armin Ronacher. Created using Sphinx.