iT邦幫忙

2021 iThome 鐵人賽

DAY 29
0
自我挑戰組

月光下的Flask之旅系列 第 29

Day 29 Unittest

在做完了程式之後,就要來測試一下是否正常運作對吧。不過當你做完了數十個 API 之後,我相信你一定不會有心情去一個一個慢慢使用 Swagger 來測試的。所以就出現了單元測試,能夠一次解決需要一個一個測試的問題。

首先,在 Python 的標準庫中(安裝 Python 時會順便安裝的),有一個 unittest 的套件,看名字也知道是用在單元測試;後來出現了 pytest 這個更加熱門的框架,同樣用於單元測試,在 Flask 官方文件中也是使用這個方式;而 Flask 也出現一個專門用於單元測試的插件 Flask-Testing,Flask-Testing 是基於 unittest 所建立的插件。

總而言之
unittest: Python 標準庫中的單元測試套件。
pytest: Python 的第三方單元測試框架。
Flask-Testing: Flask 中基於 unittest 的單元測試插件。

Flask-Testing & pytest

首先當然還是要先安裝對吧!不過因為我想講一下這兩個之間的特殊關係,需要將兩個套件一起安裝。

$ pipenv install pytest Flask-Testing

然後我們使用上一篇的架構改一下繼續使用,改完之後變這樣:

ithome
├── apis
│   ├── __init__.py
│   └── api.py
├── base
│   └── __init__.py
├── db
│   └── __init__.py
├── test  # 單元測試的東東
│   ├── __init__.py  # tests 基本的東西
│   └── test_api.py  # 測試 api.py 裡面所有 API 的檔案
├── app.py
├── config.py
├── Pipfile
└── Pipfile.lock

先來說一下測試的原理,簡單說就是拿著資料實際去做一次,然後看回傳的結果是否正確,就是這麼簡單。

test/__init__.py

from flask_testing import TestCase

from app import app
import config


class BaseTestCase(TestCase):

    def create_app(self):
		# 必要。須回傳 Flask 實體。
        app.config.from_object(config.TestingConfig)
        return app

    def setUp(self):
		# 可不寫。測試前會執行的東西,相當於 pytest 中 @pytest.fixture 這個裝飾器
		# 可以用於生出一個乾淨(沒有資料)的資料庫之類的,不過因為我是用奇怪的方式弄出類似資料庫的東東,所以就沒有寫
        pass

    def tearDown(self):
		# 可不寫。測試後會執行的東西,相當於 pytest 中 @pytest.fixture 這個裝飾器 function 內 yield 之後的程式
		# 可以用於刪除不乾淨(測試後被塞入資料)的資料庫之類的
        pass
	
	@classmethod
	def setUpClass(self):
		# 可不寫。相當於 setUp ,不過不同於 setUp 是執行一個 Function ,而是先執行一個 Class,詳細用法參考 @classmethod 或是下面的網址
		# https://docs.python.org/zh-tw/3/library/unittest.html#unittest.TestCase.setUpClass
		pass
	
	@classmethod
	def tearDownClass(self):
		# 可不寫。相當於 tearDown ,不過 setUpClass 同樣為執行一個 Class,詳細用法參考 @classmethod 或是下面的網址
		# https://docs.python.org/zh-tw/3/library/unittest.html#unittest.TestCase.tearDownClass
		pass
	
	def setUpModule():
		# 可不寫。同樣相當於 setUp ,不過不同於 setUp 以及 setUpClass 是執行一個 Function 或是 Class,而是先執行一個 Module,詳細用法參考下面的網址
		# https://docs.python.org/zh-tw/3/library/unittest.html#setupmodule-and-teardownmodule
		pass

	def tearDownModule():
		# 可不寫。相當於 tearDown ,不過 setUpModule 同樣為執行一個 Module,詳細用法參考 setUpModule 的網址
		pass

test/test_apis.py

import json

from . import BaseTestCase


class TestAccountApi(BaseTestCase):
    def test_register(self):
        # 實際送出請求
        response = self.client.post(
            '/account/register',
            data=json.dumps({
                'email': 'test01@gmail.com',
                'password': 'test'
            }),
            content_type='application/json'
        )

        # 判斷回應是否正確
        self.assertEqual(response.json, {'status': '0', 'message': ''})
        self.assertEqual(response.status_code, 200)

在這邊要注意一點,測試文件檔名必須以 test 為開頭,以及測試文件內的 Class 以及 Function 也必須以 test 開頭,否則不會自動加載(就是你想偷懶少打一些指令那就用 test 開頭,不然那個東西就要多打指令去測)。

在開始執行測試前我要先說一下為什麼用了 Flask-Testing 這個插件之後,還需要安裝 pytest 。如果你有留意 import 進來的東西的話,應該有注意到幾乎都只有用到 Flask-Testing 而已,那為什麼還需要安裝 pytest 呢?

這是因為 pytest 能夠支援 unittest 的測試寫法,而且可以少打一些測試時的執行指令與程式碼(蛤,你問說為什麼不全部改 pytest 就好,因為誰讓我先學的是 Flask-Testing 的寫法,雖然 pytest 的裝飾器也不錯用)。

然後就可以在 terminal 中輸入下方的指令開始測試(因為奇怪的資料庫寫法,導致測試沒有寫有關生出資料庫的部分,所以 redis-server 要先執行)。

$ pipenv run pytest

結果就會像這樣:

如果想要看到較詳細的東西,可以這樣輸入:

$ pipenv run pytest -v

如果想要看到較簡單的東西,可以這樣輸入:

$ pipenv run pytest -q

Coverage

雖然做完了測試,但是如何知道測試有沒有完整,如果做了數十個 API ,但是指測試了一個 API ,其他的 API 有沒有出問題也不知道。所以就必須測試覆蓋率,而 pytest 有一個測試覆蓋率的插件 pytest-cov。

同樣使用 pip 安裝。

$ pipenv install pytest-cov

安裝完後使用下面的指令就可以測試覆蓋率了。

# --cov 為指定計算覆蓋率的範圍,不加的話會連同 import 的所有套件一起計算
$ pipenv run pytest --cov=./

測完會像這樣:

雖然看出來 api.py 有 2 行沒測到(對,單位是行,包括 import 、 def 及 class ,但不含裝飾器),還有 app.py 有 1 行沒測到,但是要在大海裡摸針確實有點困難,所以就要生出一個測試報告。就要使用下面的指令。

$ pipenv run pytest --cov=./ --cov-report=html

測完的報告會以 html 格式存放在 htmlcov 裡面,打開 index.html 就可以看到總報告,像這樣:

接著點 apis/account/api.py ,就可以看到裡面有哪行沒有測試到了。像這樣:

會這樣的原因是給了正確的資料所以正常回傳,當然有錯誤發生時才會執行的程式碼當然沒有測試到,所以在 test/test_apis.py 裡面多加一個測試錯誤發生時的回傳就讓它消失了。像這樣:

    def test_register_error(self):
		# 實際送出請求
        response = self.client.post(
            '/account/register',
            data=json.dumps({}),
            content_type='application/json'
        )

        # 判斷回應是否正確
        self.assertEqual(response.json, {'status': '1', 'message': 'error'})
        self.assertEqual(response.status_code, 200)

加上了這幾行測試錯誤發生時的程式碼之後,在執行一次就會發現那邊的 miss 消失了(app.py 我不修的原因自己做完後看一下是哪行產生 miss 就知道了)。像這樣:

參考資料

Pytest测试框架(三):pytest fixture 用法

pytest文档57-计算单元测试代码覆盖率(pytest-cov)

什么是静态代码分析?

那麼就大概這樣,單元測試以及覆蓋率是開發要結束之前幾乎必須要做的最後一個動作,不過單元測試只能夠測試是否有按照預期的結果回傳而已,而覆蓋率只能夠測試多少部分的程式碼有測試過而已,並不代表是最有效率或是最簡潔的寫法。 pytest 還有能夠測試是否有按照 pep8 格式以及靜態分析的插件,pep8 只要 Day 02 有正確設定基本上不需要測了;靜態分析是啥以及須不需要測試就看上面的 什么是静態代码分析? (竟然是 MatLab ?) 說明後自行評估了 (恩對,我懶)。

大家掰~掰~


上一篇
Day 28 Flask-RESTX
下一篇
Day 30 結語
系列文
月光下的Flask之旅30

尚未有邦友留言

立即登入留言