### 導航
- [索引](# "總目錄")
- [下一頁](# "記錄應用錯誤") |
- [上一頁](# "模板") |
- [Flask 0.10.1 文檔](#) ?
# 測試 Flask 應用
> **沒有經過測試的東西都是不完整的**
這一箴言的起源已經不可考了,盡管他不是完全正確的,但是仍然離真理不遠。沒有測試過的應用將會使得提高現有代碼質量很困難,二不測試應用程序的開發者,會顯得特別多疑。如果一個應用擁有自動化測試,那么您就可以安全的修改然后立刻知道是否有錯誤。
Flask 提供了一種方法用于測試您的應用,那就是將 Werkzeug 測試[Client](http://werkzeug.pocoo.org/docs/test/#werkzeug.test.Client "(在 Werkzeug v0.10)") [http://werkzeug.pocoo.org/docs/test/#werkzeug.test.Client] 暴露出來,并且為您操作這些內容的本地上下文變量。然后您就可以將自己最喜歡的測試解決方案應用于其上了。在這片文檔中,我們將會使用Python自帶的 [unittest](http://docs.python.org/dev/library/unittest.html#module-unittest "(在 Python v3.5)") [http://docs.python.org/dev/library/unittest.html#module-unittest] 包。
### 應用程序
首先,我們需要一個應用來測試,我們將會使用 [*教程*](#) 這里的應用來演示。如果您還沒有獲取它,請從 the examples 這里查找源碼。
### 測試的大框架
為了測試這個引用,我們添加了第二個模塊(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()
flaskr.init_db()
def tearDown(self):
os.close(self.db_fd)
os.unlink(flaskr.app.config['DATABASE'])
if __name__ == '__main__':
unittest.main()
~~~
在 [setUp()](http://docs.python.org/dev/library/unittest.html#unittest.TestCase.setUp "(在 Python v3.5)") [http://docs.python.org/dev/library/unittest.html#unittest.TestCase.setUp] 方法的代碼創建了一個新的測試客戶端并且初始化了一個新的數據庫。這個函數將會在每次獨立的測試函數運行之前運行。要在測試之后刪除這個數據庫,我們在 [tearDown()](http://docs.python.org/dev/library/unittest.html#unittest.TestCase.tearDown "(在 Python v3.5)") [http://docs.python.org/dev/library/unittest.html#unittest.TestCase.tearDown]函數當中關閉這個文件,并將它從文件系統中刪除。同時,在初始化的時候TESTING 配置標志被激活,這將會使得處理請求時的錯誤捕捉失效,以便于您在進行對應用發出請求的測試時獲得更好的錯誤反饋。
這個測試客戶端將會給我們一個通向應用的簡單接口,我們可以激發對向應用發送請求的測試,并且此客戶端也會幫我們記錄 Cookie 的動態。
因為 SQLite3 是基于文件系統的,我們可以很容易的使用臨時文件模塊來創建一個臨時的數據庫并初始化它,函數 [mkstemp()](http://docs.python.org/dev/library/tempfile.html#tempfile.mkstemp "(在 Python v3.5)") [http://docs.python.org/dev/library/tempfile.html#tempfile.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.DATABASE)
def test_empty_db(self):
rv = self.app.get('/')
assert 'No entries here so far' in rv.data
~~~
注意到我們的測試函數以 test 開頭,這允許 [unittest](http://docs.python.org/dev/library/unittest.html#module-unittest "(在 Python v3.5)") [http://docs.python.org/dev/library/unittest.html#module-unittest] 模塊自動識別出哪些方法是一個測試方法,并且運行它。
通過使用 self.app.get 我們可以發送一個 HTTP GET 請求給應用的某個給定路徑。返回值將會是一個 [response_class](# "flask.Flask.response_class")對象。我們可以使用 [data](http://werkzeug.pocoo.org/docs/wrappers/#werkzeug.wrappers.BaseResponse.data "(在 Werkzeug v0.10)") [http://werkzeug.pocoo.org/docs/wrappers/#werkzeug.wrappers.BaseResponse.data] 屬性來檢查程序的返回值(以字符串類型)。在這里,我們檢查 'Noentriesheresofar'是不是輸出內容的一部分。
再次運行,您應該看到一個測試成功通過了:
~~~
$ 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 '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
~~~
這里我們測試計劃的行為是否能夠正常工作,即在正文中可以出現 HTML標簽,而在標題中不允許。
運行這個測試,我們應該得到三個通過的測試:
~~~
$ python flaskr_tests.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.332s
OK
~~~
關于請求的頭信息和狀態值等更復雜的測試,請參考[MiniTwit Example](http://github.com/mitsuhiko/flask/tree/master/examples/minitwit/) [http://github.com/mitsuhiko/flask/tree/master/examples/minitwit/] ,在這個例子的源代碼里包含一套更長的測試。
### 其他測試技巧
除了如上文演示的使用測試客戶端完成測試的方法,也有一個[test_request_context()](# "flask.Flask.test_request_context") 方法可以配合 with 語句用于激活一個臨時的請求上下文。通過它,您可以訪問 [request](# "flask.request") 、[g](# "flask.g")和 [session](# "flask.session") 類的對象,就像在視圖中一樣。這里有一個完整的例子示范了這種用法:
~~~
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
assert flask.request.path == '/'
assert flask.request.args['name'] == 'Peter'
~~~
所有其他的和上下文綁定的對象都可以使用同樣的方法訪問。
如果您希望測試應用在不同配置的情況下的表現,這里似乎沒有一個很好的方法,考慮使用應用的工廠函數(參考 [*應用程序的工廠函數*](#))
注意,盡管你在使用一個測試用的請求環境,函數[before_request()](# "flask.Flask.before_request") 以及[after_request()](# "flask.Flask.after_request") 都不會自動運行。然而,[teardown_request()](# "flask.Flask.teardown_request") 函數在測試請求的上下文離開 with 塊的時候會執行。如果您希望 [before_request()](# "flask.Flask.before_request") 函數仍然執行。您需要手動調用 [preprocess_request()](# "flask.Flask.preprocess_request") 方法:
~~~
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
app.preprocess_request()
...
~~~
這對于打開數據庫連接或者其他類似的操作來說,很可能是必須的,這視您應用的設計方式而定。
如果您希望調用 [after_request()](# "flask.Flask.after_request") 函數,您需要使用 [process_response()](# "flask.Flask.process_response") 方法。這個方法需要您傳入一個 response 對象:
~~~
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
resp = Response('...')
resp = app.process_response(resp)
...
~~~
這通常不是很有效,因為這時您可以直接轉向使用測試客戶端。
### 偽造資源和上下文
0.10 新版功能.
在應用上下文或 [flask.g](# "flask.g") 對象上存儲用戶認證信息和數據庫連接非常常見。一般的模式是在第一次使用對象時,把對象放在應用上下文或[flask.g](# "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](# "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)
~~~
### 保存上下文
0.4 新版功能.
有時,激發一個通常的請求,但是將當前的上下文保存更長的時間,以便于附加的內省發生是很有用的。在 Flask 0.4 中,通過 [test_client()](# "flask.Flask.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()](# "flask.Flask.test_client") 方法,而不使用 with 代碼塊, assert 斷言會失敗,因為 request不再可訪問(因為您試圖在非真正請求中時候訪問它)。
### 訪問和修改 Sessions
0.8 新版功能.
有時,在測試客戶端里訪問和修改 Sesstions 可能會非常有用。通常有兩種方法實現這種需求。如果您僅僅希望確保一個 Session擁有某個特定的鍵,且此鍵的值是某個特定的值,那么您可以只保存起上下文,并且訪問 [flask.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](# "flask.session") 代理,而這個對象本身提供了同樣的接口。
? 版權所有 2013, Armin Ronacher.
- 歡迎使用 Flask
- 前言
- 給有經驗程序員的前言
- 安裝
- 快速入門
- 教程
- 介紹 Flaskr
- 步驟 0: 創建文件夾
- 步驟 1: 數據庫模式
- 步驟 2: 應用設置代碼
- 步驟 3: 創建數據庫
- 步驟 4: 請求數據庫連接
- 步驟 5: 視圖函數
- 步驟 6: 模板
- 步驟 7: 添加樣式
- 福利: 應用測試
- 模板
- 測試 Flask 應用
- 記錄應用錯誤
- 配置處理
- 信號
- 即插視圖
- 應用上下文
- 請求上下文
- 用藍圖實現模塊化的應用
- Flask 擴展
- 與 Shell 共舞
- Flask 代碼模式
- 大型應用
- 應用程序的工廠函數
- 應用調度
- 使用 URL 處理器
- 部署和分發
- 使用 Fabric 部署
- 在 Flask 中使用 SQLite 3
- 在 Flask 中使用 SQLAlchemy
- 上傳文件
- 緩存
- 視圖裝飾器
- 使用 WTForms 進行表單驗證
- 模板繼承
- 消息閃現
- 用 jQuery 實現 Ajax
- 自定義錯誤頁面
- 延遲加載視圖
- 在 Flask 中使用 MongoKit
- 添加 Favicon
- 數據流
- 延遲請求回調
- 添加 HTTP Method Overrides
- 請求內容校驗碼
- 基于 Celery 的后臺任務
- 部署選擇
- mod_wsgi (Apache)
- 獨立 WSGI 容器
- uWSGI
- FastCGI
- CGI
- 聚沙成塔
- API
- JSON 支持
- Flask 中的設計決策
- HTML/XHTML 常見問題
- 安全注意事項
- Flask 中的 Unicode
- Flask 擴展開發
- Pocoo 風格指引
- Python 3 支持
- 升級到最新版本
- Flask Changelog
- 許可證
- 術語表