04 Python Flask 教學10 所有文章

【Flask 教學】實作 Flask 單元測試 Unit Test

發佈於
Flask 單元測試 unittest_Max行銷誌

一. 環境設置

Flask 套件選擇和安裝

此次選擇使用的是 Flask-Testing,此套件將 Python 內建的 Unittest 進行封裝,相較於 pytest 優點在建立 create_app 的時候非常簡單易懂。

Flask-Testing — Flask-Testing 0.3 documentation

pip3 install Flask-Testing

此次架構配置

會在專案中建立新資料夾 tests,並將所有的單元測試 py 檔放置於此資料夾內,此次架構上會搭配 Flask 工廠模式使用。

有關於 flask 工廠模式的好處和使用方法,可以參考此篇:
【Flask 教學】實作 Flask Application Factories 工廠模式 | Max行銷誌

└── flask
   ├── app
   │   ├── __init__.py
   │   ├── config
   │   ├── model
   │   └── view
   ├── main.py
   └── tests
       └── test_auth.py

二. 進入主題 Flask 實作單元測試

實作步驟一. 配置 main.py

我們將使用 Flask 的 CLI (Command Line Interface — Flask Documentation (1.1.x) ),CLI 的好處是可以在終端機中運行設定好的 Flask 指令,如下範例程式將命名函式為 test,所以我們只要在終端機中,先 cd 到 main.py 的位置,再下指令 flask test 就會執行以下設定好的指令,開始運行所有測試囉!

main.py

from flask_migrate import Migrate, upgrade, migrate
from app import create_app, db

app = create_app('development')
migrates = Migrate(app=app, db=db)

@app.cli.command()
def test():
    import unittest
    import sys

    tests = unittest.TestLoader().discover("tests")
    result = unittest.TextTestRunner(verbosity=2).run(tests)
    if result.errors or result.failures:
        sys.exit(1)

實作步驟二. 配置 test 檔

只要將想要運行的 py 檔,放置於此 tests 文件中,待會運行指令時就會被運行到,以下我們將寫一個範例 test_auth.py 檔,此份將會測試使用者登入的 api 運行。

tests/test_auth.py

import unittest
from flask import url_for
from flask_testing import TestCase
from app import create_app, db
import json

class SettingBase(TestCase):
    def create_app(self):
        return create_app("testing")

      # 在運行測試之前會先被執行
    def setUp(self):
        db.create_all()
        self.username = "[email protected]"
        self.passwords = "666666"
        self.role = 0

      # 在結束測試時會被執行
    def tearDown(self):
        db.session.remove()
        db.drop_all()

      # signup 是測試時很常會被用到的功能,所以寫成函式,可以重複利用
    def signup(self):
        response = self.client.post(url_for('api.users'),
                                    follow_redirects=True,
                                    json={
                                        "username": self.username,
                                        "password": self.passwords,
                                        "role": self.role
                                    })
        return response


# 這邊繼承剛剛的寫的 SettingBase class,接下來會把測試都寫在這裡
class CheckUserAndLogin(SettingBase):
    def test_signup(self):
        response = self.signup()
        self.assertEqual(response.status_code, 200)

    def test_signup_400(self):
        # 測試密碼少於六位數
        self.passwords = '123'
        response = self.signup()
        self.assertEqual(response.status_code, 400)

    def test_signup_422(self):
        # 測試重複註冊
        response = self.signup()
        response = self.signup()
        self.assertEqual(response.status_code, 422)


if __name__ == '__main__':
    unittest.main()
解析 1. 實例化 Flask instance

我們使用 Flask 工廠模式,所以從 app/__init__.py 中 import create_app,所以只需要 return create_app("testing")

    def create_app(self):
        return create_app("testing")

如果不使用工廠模式的話,就需要實例化的 app = Flask(__name__),並設定 config 參數

    def create_app(self):
        app = Flask(__name__)
        app.config['TESTING'] = True
        return app
解析 2. 測試前後工作準備

此外我們在 SettingBase 的 class 中有額外設定 setUp()tearDown() 方法,setUp() 代表在運行測試之前會先被執行,而 tearDown() 代表在結束測試時會被執行。

      # 在運行測試之前會先被執行
    def setUp(self):
        db.create_all()
        self.username = "[email protected]"
        self.passwords = "666666"
        self.role = 0

      # 在結束測試時會被執行
    def tearDown(self):
        db.session.remove()
        db.drop_all()
解析 3. 斷言 Assertion 的使用方法
  • assertEqual(a, b):a == b
  • assertNotEqual(a, b):a != b
  • assertTrue(x):bool(x) is True
  • assertFalse(x):bool(x) is False
  • assertIs(a, b):a is b
  • assertIsNot(a, b):a is not b
  • assertIsNone(x):x is None
  • assertIsNotNone(x):x is not None
  • assertIn(a, b):a in b
  • assertNotIn(a, b):a not in b
  • response.data:看回傳「結果」
  • response.status_code:想看回傳「狀態」

更多詳細 assertIs 指令可以參考此篇:unittest — 單元測試框架 — Python 3.8.5 說明文件

實作步驟三. 運行測試

因為我們有在 main.py 寫入 CLI,所以只要終端機與 /flask/main.py 同位置,只需要下指令 flask test 就會開始運行測試

@app.cli.command()
def test():
    import unittest
    import sys

    tests = unittest.TestLoader().discover("tests")
    result = unittest.TextTestRunner(verbosity=2).run(tests)
    if result.errors or result.failures:
        sys.exit(1)

運行結果如下:

test_signup (test_auth.CheckUserAndLogin) ... ok
test_signup_400 (test_auth.CheckUserAndLogin) ... ok
test_signup_422 (test_auth.CheckUserAndLogin) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.402s

OK

補充. 覆蓋率測試

在終端機輸入以下指令,和剛剛一樣他會先偵測 /tests 裡面的所有的 py 檔,然後開始運行每一份測試檔,並將報告結果存在 /tests/test.coveragerc

coverage run --rcfile= /tests/test.coveragerc -m unittest discover -s tests/
coverage report -m
coverage html --rcfile= /tests/test.coveragerc

運行結果

...
----------------------------------------------------------------------
Ran 3 tests in 0.465s

OK
Name                            Stmts   Miss  Cover   Missing
-------------------------------------------------------------
flask/tests/test_auth.py           3      0     100%
-------------------------------------------------------------
TOTAL                              3      0    100%

補充:單元測試 Unit Test 原則

以下原則參考:Test F.I.R.S.T | Hacker Noon

F.I.R.S.T 原則 (F.I.R.S.T Principles of Unit Testing)
1. Fast – 快速
2. Independent – 獨立,測試之間要相互獨立,如果互相依賴的話,一個測試失敗會影響其他測試也都失敗。
3. Repeatable – 可重複,要在任何環境都可重複執行。
4. Self-Validating – 顯示驗證結果,可從 report 直接了解失敗原因
5. Timely – 及時,最好是在寫程式之前先寫測試 (TDD 概念)

文章中有提到在考量 Independent 時,會遵循 3A rule:
1. Arrange:建立此測試案例需要的初始值,和思考好的命名和變數命稱來讓測試更容易理解。
2. Act:呼叫目標方法
3. Assert:驗證是否符合預期

最後~

▍回顧本篇 Flask Unittest 文章:

  • 環境設置
    • Flask 套件選擇和安裝
    • 此次架構配置
  • 進入主題 Flask 實作單元測試
    • 實作步驟一. 配置 main.py
    • 實作步驟二. 配置 test 檔
    • 實作步驟三. 運行測試
  • 補充. 覆蓋率測試
  • 補充:單元測試 Unit Test 原則

更多 Flask 教學相關閱讀:

▍關於 Flask 教學系列目錄:

▍關於 Flask 部署相關文章:

▍其他 Flask 相關教學:

有關 Max行銷誌的最新文章,都會發佈在 Max 的 Facebook 粉絲專頁,如果想看最新更新,還請您按讚或是追蹤唷!

在〈【Flask 教學】實作 Flask 單元測試 Unit Test〉中有 1 則留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *