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