今天要開始寫測試,這個部份我們不會特別認真寫,重點是要把比較常用的函式秀出來。我們會用最原始的 unittest 套件來寫,當然也可以自己改成 pytest 等等其他測試框架。
雖然說是前置作業,但這個步驟放到最後也沒有關係。我們要來在 manage.py
再加入一個新指令。當然要記得 import unittest
。
@app.cli.command()
def test():
tests = unittest.TestLoader().discover("tests")
unittest.TextTestRunner().run(tests)
這是一段 unittest 很常見的程式碼,基本上他會找到之前已經建好的 tests/
然後跑裡面全部的測試。
這樣一來,等等測試寫完之後就可以使用 flask test
來跑測試了。
我們今天會寫出 basic test 跟藍圖 main_bp
的測試,先從前者開始。以下程式碼要放在 tests/test_basic.py
裡面。
import unittest
from flask import url_for
from app import create_app
class BasicTest(unittest.TestCase):
def setUp(self) -> None:
self.app = create_app("testing")
self.client = self.app.test_client()
self.app_context = self.app.app_context()
self.app_context.push()
def tearDown(self) -> None:
if self.app_context:
self.app_context.pop()
def test_app_is_alive(self):
response = self.client.get(url_for("main.index_page"))
self.assertEqual(response.status_code, 200)
def test_blueprint(self):
self.assertNotEqual(self.app.blueprints.get("main", None), None)
self.assertNotEqual(self.app.blueprints.get("user", None), None)
self.assertNotEqual(self.app.blueprints.get("admin", None), None)
我們在 setUp
裡面先用 create_app
建好一個 app
,接著使用了 test_client
,他是一個讓我們可以模擬客戶端的工具,可以使用他來對 app
發 request。接下來的兩行跟 app_context
有關,基本上它就是把一個環境套用到裡面,這樣之後的程式碼都會在這個環境之下。然後因為在 setUp
我們套用了 app_context
,那在結束 tearDown
的時候就要把他 pop
掉,這樣才不會影響之後的測試。
如果覺得有點難懂的話,可以打開 python,然後直接呼叫 current_app.config
,他會告訴你 Working outside of application context.
,這時候如果你給他一個 app.app_context().push()
(當然 app
要自己宣告) 再呼叫一次,就可以看到他沒有錯誤然後給你一個好好的設定檔,就像我們之前看到的那樣,而 pop
掉之後他又會變成跑出錯誤。在這裡做的事就接近上述的行為,只是把在同一個 python interpreter 的環境變成在同一次測試的環境。
接下來我們繼續看到後面,這裡有兩個測試,第一個是透過抓首頁來確定 app
有沒有好好活著;第二個是確定藍圖有沒有被好好的載入。我們分開來說明。
test_app_is_alive
中,我們使用了剛剛宣告的 self.client
,並使用了他的 get
函式,這個跟 HTTP method 的那個 get 是同一個,所以可想而知,後面一定會有 post、put 等等類似的函式。而他會回傳一個 response
,我們可以來檢查他的回應有沒有符合預期。此處我們去看他的 status_code
是不是 200。test_blueprint
裡面,我們去檢查 self.app.blueprints
裡面有沒有該有的藍圖,他是 dict 型別,所以可以這樣去確認。因為測試很多函式的重複性很高,所以在開始寫 main_bp
的測試前,我們先來寫一下 helper.py
,一樣要放在 tests/
裡面。
import unittest
from flask import url_for
from app import create_app
from app.database import db, add_user
class TestModel(unittest.TestCase):
def setUp(self) -> None:
self.app = create_app("testing")
self.client = self.app.test_client()
self.app_context = self.app.app_context()
self.app_context.push()
self.user_data = {"username": "user", "password": "user"}
self.admin_data = {"username": "admin", "password": "admin"}
generate_test_data()
def tearDown(self) -> None:
db.session.remove()
db.drop_all()
if self.app_context is not None:
self.app_context.pop()
def user_login(self):
return self.client.post(url_for("user.login_page"), data=self.user_data)
def admin_login(self):
return self.client.post(url_for("user.login_page"), data=self.admin_data)
def login(self, login):
if login == "user":
self.user_login()
if login == "admin":
self.admin_login()
def get(self, login=False):
self.login(login)
res = self.client.get(self.route, follow_redirects=True)
return res
def post(self, login=False, data=None):
self.login(login)
res = self.client.post(self.route, data=data, follow_redirects=True)
return res
def generate_test_data():
db.create_all()
add_user("user", "user", "user@user.com")
add_user("admin", "admin", "admin@admin.com", is_admin=True)
在這裡面我們定義了一個 TestModel
,我們之後的測試都會換成繼承他,而非剛剛的 unittest.TestCase
,也因為如此,接下來定義的函式都不是測試,而是給測試用的工具。在 setUp
還有 tearDown
跟剛剛做的事差不多,有差別的部份是我們新增了 user_data
和 admin_data
,這在之後登入會用到。同時我們也用之前寫好的 add_user
來加入兩個測試用的使用者,如果有需要測試貼文、留言的話,也可以自己加入測試的資料。還有我們也在 tearDown
加入了清除資料庫的動作。
接著我們定義了 user_login
和 admin_login
,這邊就用到剛剛提到的 post
這個函式,然後他使用 data
參數把登入資料丟進去。再用 login
把上述兩者包裝起來給之後的函式用。
接下來我們自己定義了 get
和 post
兩個函式,讓它包含登入的功能,登入有很多種寫法,但有時候並沒有那麼直觀,可能會遇到明明登入了但後面發請求的時候又變成沒登入,這常常都是因為 context 不對。post
的部分跟剛剛一樣都使用 data
參數來把資料傳送給後端。我們還用到了一個叫做 follow_redirects
的參數,如果不讓他 follow 的話,那 status_code
就會變成 302
,然後我們看到的資料也都是重新導向頁面的資料,而這通常不是我們樂見的 (除非我們只想確定他有重新導向),因此在此處我們加上這個參數。