iT邦幫忙

2021 iThome 鐵人賽

DAY 15
0
Modern Web

Flask系列 第 15

Day 15 實作測試 (1)

前言

今天要開始寫測試,這個部份我們不會特別認真寫,重點是要把比較常用的函式秀出來。我們會用最原始的 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

我們今天會寫出 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 型別,所以可以這樣去確認。

helper

因為測試很多函式的重複性很高,所以在開始寫 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_dataadmin_data,這在之後登入會用到。同時我們也用之前寫好的 add_user 來加入兩個測試用的使用者,如果有需要測試貼文、留言的話,也可以自己加入測試的資料。還有我們也在 tearDown 加入了清除資料庫的動作。

接著我們定義了 user_loginadmin_login,這邊就用到剛剛提到的 post 這個函式,然後他使用 data 參數把登入資料丟進去。再用 login 把上述兩者包裝起來給之後的函式用。

接下來我們自己定義了 getpost 兩個函式,讓它包含登入的功能,登入有很多種寫法,但有時候並沒有那麼直觀,可能會遇到明明登入了但後面發請求的時候又變成沒登入,這常常都是因為 context 不對。post 的部分跟剛剛一樣都使用 data 參數來把資料傳送給後端。我們還用到了一個叫做 follow_redirects 的參數,如果不讓他 follow 的話,那 status_code 就會變成 302,然後我們看到的資料也都是重新導向頁面的資料,而這通常不是我們樂見的 (除非我們只想確定他有重新導向),因此在此處我們加上這個參數。


上一篇
Day 14 實作 database migration
下一篇
Day 16 實作測試 (2)
系列文
Flask30

尚未有邦友留言

立即登入留言