第122天:Flask 單元測試

圖片

若是一個軟件項目沒有通過測試,就像作的菜裏沒加鹽同樣。Flask 做爲一個 Web 軟件項目,如何作單元測試呢,今天咱們來了解下,基於 unittest 的 Flask 項目的單元測試。html

什麼是單元測試

單元測試是軟件測試的一種類型。顧名思義,單元測試的對象是程序中的最小的單元,能夠是一個函數,一個類,也能夠是它們的組合。python

相對於模塊測試、集成測試以及系統測試等高級別的測試,單元測試通常由軟件開發者而不是獨立的測試工程師完成,且具備自動化測試的特質,所以單元測試也屬於自動化測試。git

在實際開發中,有一些測試建議:github

  • 測試單元應該關注於盡量小的功能,要能證實它是正確的
  • 每一個測試單元必須是徹底獨立的,必須能單獨運行
  • 修改代碼後,須要從新執行一次測試代碼,以確保本次修改不會影響到其餘部分
  • 提交代碼前,須要執行一次完整測試,以確保不會將不完整或者錯誤的代碼提交,影響其餘開發者
  • 測試代碼要和正常代碼有明顯的區分,測試代碼文件應該是獨立的

unittest 模塊

Python 有不少單元測試框架,unittest、nose、pytest 等等,unittest 是 Python 內置的測試庫,也是不少測試框架的基礎,地位如同 Java 中的 JUnit,因此有時也被稱做 PyUnit。數據庫

unittest 支持 自動化測試、能夠在多個測試中 共享設置測試環境和撤銷測試環境代碼能夠將分散的測試集中起來,而且能夠支持多種測試報告框架,所以 unittest 有四種重要概念:編程

  • test fixture 測試先後須要作些準備和清理工做,例如臨時數據庫鏈接、測試數據建立、測試用服務器建立,以及測試後的清理和銷燬,test fixture 提供了 setUptearDown 接口來完成這些事情,而且能夠被多個測試方法所共享
  • test case 測試用例,是最小的測試單元,檢測一個特定輸入的響應結果,unittest 提供 TestCase 基類,以便開發者建立具體的測試用例類
  • test suite 暫且翻譯成測試套餐吧,是多個測試用例、測試套餐的組合,爲了將一組相關的測試組織起來的工具
  • test run 測試執行器是按照必定規則執行測試用例,記錄並返回測試結果的組件

小試牛刀

unittest 不須要安裝,直接導入,例如一個測試字符串方法的測試代碼:json

import unittest
class TestStringMethods(unittest.TestCase):
   def test_upper(self):        self.assertEqual('foo'.upper(), 'FOO')
   def test_isupper(self):        self.assertTrue('FOO'.isupper())        self.assertFalse('Foo'.isupper())
   def test_split(self):        s = 'hello world'        self.assertEqual(s.split(), ['hello', 'world'])        # check that s.split fails when the separator is not a string        with self.assertRaises(TypeError):            s.split(2)
if __name__ == '__main__':    unittest.main()
  • 導入 unittest 模塊
  • 建立一個測試字符串方法的測試類,繼承之 unittest 的 TestCase
  • 編寫測試方法,注意測試方法必須以 test 做爲開頭,這樣才能被測試加載器識別,同時也是良好的編程習慣
  • TestCase 提供了不少檢驗方法,例如 assertEqualassertTrue 等等,用於對指望結果進行檢測
  • 最後,若是最爲主代碼被運行,調用 unittest.main 執行全部測試方法

運行代碼:flask

python testBase.py

或者數組

python -m unittest testBase.py

結果以下:瀏覽器

...----------------------------------------------------------------------Ran 3 tests in 0.000s
OK

能夠看到,執行了三個測試,沒有發現異常狀況,. 表示測試經過,數量表示執行了的測試方法個數

測試執行器

unittest.main 只給出了概要測試結果,若是須要更詳細的報告,能夠用測試執行器來運行測試代碼

將 unittest.main() 換成:

suite = unittest.TestLoader().loadTestsFromTestCase(TestStringMethods)unittest.TextTestRunner(verbosity=2).run(suite)
  • 利用測試加載器(TestLoader)建立了一個測試套餐(TestSuite)
  • 用測試執行器(TestRunner)執行測試代碼
  • TestTestRunner 是將結果做爲文本格式輸出
  • 參數 verbosity=2 表示顯示詳細的測試報告

或者乾脆爲 unittest.main 提供參數 verbosity :unittest.main(verbosity=2)

運行結果以下:

test_isupper (__main__.TestStringMethods) ... oktest_split (__main__.TestStringMethods) ... oktest_upper (__main__.TestStringMethods) ... ok
----------------------------------------------------------------------Ran 3 tests in 0.000s
OK

Flask 單元測試

Flask 做爲一個 Web 項目,大多數代碼須要在 Web 服務器環境下運行

  • 因此須要爲每一個單元測試模擬一個 Web 環境
  • 另外有些部分須要使用到數據庫,因此還須要爲這些測試準備一個數據庫環境
  • 最後有些業務處理代碼,好比加工數據,數據運算等,能夠進行獨立測試,不須要 Web 環境

建立了一個簡單項目,經過工廠方法建立 Flask 應用,有數據庫的讀寫,下面逐步說明下測試腳本,測試代碼文件 testApp.py 與項目代碼在同一目錄下

初始化環境

import unittest
from app import create_appfrom model import db
class TestAPP(unittest.TestCase):    def setUp(self):        self.app = create_app(config_name='testing')        self.client = self.app.test_client()        with self.app.app_context():            db.create_all()
   def tearDown(self):        with self.app.app_context():            db.drop_all()
  • 引入 unittest 模塊
  • 從 Flask 應用代碼文件(app.py)中引入工廠方法 create_app
  • 從模型代碼文件(model.py)中引入數據庫實例 db
  • 建立測試類 TestAPP,繼承自 unittest.TestCase
  • 定義 setUp 方法,用工廠方法初始化 Flask 應用
  • Flask 提供了測試應用的建立方法 test_client,返回測試應用實例
  • 在應用實體環境下,初始化數據庫
  • 定義 tearDown 方法,在測試結束後銷燬數據庫中的結構和數據

簡單測試

編寫兩個測試方法,分別對 Flask 應用的配置狀況和首頁進行測試:

def test_config(self):        self.assertEqual(self.app.config['TESTING'], True)        self.assertIsNotNone(self.app.config['SQLALCHEMY_DATABASE_URI'])
def test_index(self):    ret = self.client.get('/')    self.assertEqual(b'Hello world!', ret.data)
  • 定義測試方法 test_config 用來測試 Flask app 的配置是否正常
  • 由於測試方法時實體方法,因此從實體引用(self)中的 app 屬性中,查看配置屬性,注意測試應用 test_client 不能之間獲取 Flask app 的配置
  • 檢測 TESTING 的值是否爲 True,另外檢查數據庫鏈接是否存在
  • 定義方法首頁的方法 test_index,經過測試應用的 get 方法訪問網站根目錄
  • 檢測訪問後的結果,在示例中,首頁返回了字符串,確認下是否正確

此時運行測試代碼能夠獲得以下

test_config (__main__.TestAPP) ... oktest_index (__main__.TestAPP) ... ok
----------------------------------------------------------------------Ran 2 tests in 0.066s

測試表單提交

在 Web 項目中,有不少須要交互的功能,例如表單提交,數據存儲和查詢,在 unittest 測試框架中,藉助 Flask 的測試應用 test_client 能夠輕鬆應對

示例項目中,有模擬用戶註冊和登陸的功能,註冊和登陸都須要提交數據,而且只有在註冊後,才能進行登陸,因此將註冊和登陸編寫成單獨的功能:

def login(self, username):    params = {'username': username}    return self.client.post('/login', data=params, follow_redirects=True)
def register(self, username):    params = {'username': username}    return self.client.post('/register', data=params, follow_redirects=True)
  • 定義登陸方法 login,接受一個用戶名的參數(這裏忽略了密碼等登陸憑證)
  • 利用測試應用 test_client 的 post 方法,訪問登陸地址,將提交的數據用詞典數據結構經過 data 參數提交
  • 定義註冊方法 register,接受一個用戶名的參數(一樣忽略了密碼等其餘信息)
  • 註冊方法和登陸相似,除了註冊提交地址
  • 注意到 post 的參數 follow_redirects,值爲 True 的做用是支持瀏覽器跳轉,即收到跳轉狀態碼時會自動跳轉,直到不是跳轉狀態碼時纔會返回
  • 登陸和註冊方法能夠處理更多的業務邏輯,最後將請求結果返回

有了註冊和登陸的協助,測試方法就更明晰:

def test_register(self):    ret = self.register('bar')    self.assertEqual(json.loads(ret.data)['success'], True)
def test_login(self):    self.register('foo')    ret = self.login('foo')    return self.assertEqual(json.loads(ret.data)['username'], 'foo')
def test_noRegisterLogin(self):    ret = self.login('foo')    return self.assertEqual(json.loads(ret.data)['success'], False)
def test_login_get(self):    ret = self.client.get('/login', follow_redirects=True)    self.assertIn(b'Method Not Allowed', ret.data)
  • 定義了 4 個測試方法,分別時單獨的註冊,註冊後登陸,未註冊時的登陸,和用 get 方法請求登陸接口
  • 每種方法都調用了 login 或者 register 方法,因此代碼邏輯會更簡潔
  • 註冊和登陸接口,返回的時 JSON 格式數據,須要用 json.loads 將其轉化爲 詞典
  • assertIn 相似與 indexOf 方法,用來檢測給定的字符串是否在結果中

運行上述的是測試,能夠獲得以下結果:

test_login (__main__.TestAPP) ... oktest_login_get (__main__.TestAPP) ... oktest_noRegisterLogin (__main__.TestAPP) ... oktest_register (__main__.TestAPP) ... ok
----------------------------------------------------------------------Ran 4 tests in 0.196s
OK

您可能已經發現,測試執行的結果和測試方法定義的順序不一致

緣由是測試加載器是按照測試名稱字母順序加載測試方法的,若是須要按照必定的順序執行,須要用 TestSuite 設定執行順序,如:

if __name__ == '__main__':    suite = unittest.TestSuite()    tests = [TestAPP('test_register'), TestAPP('test_login'), TestAPP('test_noRegisterLogin'), TestAPP('test_login_get')]    suite.addTests(tests)    runner = unittest.TextTestRunner(verbosity=2)    runner.run(suite)
  • 建立 TestSuite 實例
  • 將須要組織的測試方法放在數組中,用 TestSuiteaddTests 方法添加到 TestSuite 實例中
  • TestRuuner 運行 TestSuite 實例

這樣就會以設定的順序執行測試方法了

代碼覆蓋率

測試中有個重要的概念就是代碼覆蓋率,若是存在沒有被被覆蓋的代碼,就有可能編寫的測試代碼不夠全面

coverage Python 的一個測試工具,不只能夠運行測試代碼,還能夠報告出代碼覆蓋率

安裝

使用前,須要安裝:

pip install coverage

執行測試

安裝成功後,就能夠在命令行中使用了,首先進入到測試代碼的所在目錄,

請注意  Python  包引用的查找位置,從不一樣的目錄運行,可能會影響到目錄下模塊的引用,例如在同一目錄下,引用模塊,若是在上一級目錄中運行代碼,可能出現找不到模塊的錯誤,此時只須要相對於運行目錄,調整下代碼中模塊引用方式就行了,具體可參見Python  Unit Testing – Structuring Your Project

執行以下命令:

coverage run testApp.py

結果以下:

test_config (__main__.TestAPP) ... oktest_index (__main__.TestAPP) ... oktest_login (__main__.TestAPP) ... oktest_login_get (__main__.TestAPP) ... oktest_noRegisterLogin (__main__.TestAPP) ... oktest_register (__main__.TestAPP) ... ok
----------------------------------------------------------------------Ran 6 tests in 0.226s

結果和之間運行測試代碼相似,也就是說用 coverage run 命令能夠代替 python 命令執行測試代碼,例如

python -m unittest discover

將變爲

coverage run -m unittest discover

覆蓋率

coverage 更大的用處在於查看代碼覆蓋率,命令是 coverage report,例如:

coverage report testApp.py

結果以下:

Name         Stmts   Miss  Cover--------------------------------testApp.py      41      0   100%
  • Name 指的是代碼文件名
  • Stmts 是執行的代碼行數
  • Miss 表示沒有被執行的行數
  • Cover 表示覆蓋率,公式是(Stmts-Miss)/Stmts,即被執行代碼所佔比例,用百分比表示

若是要看到哪些行被忽略了,加上參數 -m 便可:

coverage report -m testApp.py

結果中會多一列 Missing,內容爲執行的行號

代碼覆蓋率報告,是基於 coverage run 的運行結果的,因此沒有測試的運行就沒法獲得覆蓋率報告的

總體覆蓋率報告

coverage run 在執行測試時,會記錄全部被調用代碼文件的執行狀況,包括 Python 庫中的代碼,若是隻想記錄指定目錄下的代碼執行狀況,須要用 --source 選項指定須要記錄的目錄,例如只記錄當前目錄下的執行狀況:

coverage run --source . testApp.py

而後查看執行報告,例如:

Name         Stmts   Miss  Cover--------------------------------app.py          10      0   100%config.py       17      1    94%model.py        17      4    76%route.py        19      1    95%testApp.py      41      0   100%--------------------------------TOTAL          104      6    94%

若是執行時沒有加上 --source 參數,也能夠經過通配符文件名,指定要查看的代碼文件:

coverage report *.py

結果同上

html 測試報告

若是項目中代碼文件衆多,在命令行中用文本方式顯示測試報告就不太方便了,coverage html 能夠將測試報告生成 html 文件,功能強大,顯示效果更好:

coverage html -d testreport

參數 -d 用來指定測試報告存放的目錄,若是不存在會建立

圖片

文件名是個鏈接,點擊能夠看到文件內容,而且將執行和未執行的代碼標註的很清楚:

圖片

總結

今天介紹了 Flask 的單元測試,主要介紹了 Python 自帶單元測試模塊 unittest 的基本用法,以及 Flask 項目中單元測試的特色和方法,還介紹了 coverage 測試工具,以及代碼覆蓋率報告的用法。

最後須要強調的是:不管什麼軟件項目,單元測試是頗有必要的,單元測試不只能夠確保項目的高質量交付,並且還爲維護和查找問題節省了時間。

參考

https://medium.com/@neeti.jain/how-to-do-unit-testing-in-flask-and-find-code-coverage-fa5201399bc4https://www.patricksoftwareblog.com/python-unit-testing-structuring-your-project/https://coverage.readthedocs.io/en/coverage-5.0.3/index.htmlhttps://testerhome.com/topics/11655

示例代碼:https://github.com/JustDoPython/python-100-day/tree/master/day-122


系列文章


第121天:機器學習之決策樹
從 0 學習 Python 0 - 120 大合集總結

相關文章
相關標籤/搜索