- 取得連結
- X
- 以電子郵件傳送
- 其他應用程式
程式語言:Python
範例 Source code
功能:Python test
可跳過測試,並且可設定特定條件決定是否跳過
期望會失敗的測試,但不跳過,若成功則會是 XPASS 狀態
也可設定條件,決定是否視為 xfail
當 xfail_strict=true 時,XPASS 也會視為 Fail,可在 pytest.ini 內設定
傳入參數測試,list 會逐項代入,可設定 ids 讓報表更好看
但在 ids_func 則是逐元素代入,這點要注意
Testing a Flask Application using pytest
Testing Flask SQLAlchemy database with pytest
Python Testing with pytest
- Package:
- pytest
範例 Source code
功能:Python test
基本探索規則
- 檔名為 test_*.py 或 *_test.py
- method or function 的名字前綴為 test_*
- 位於 class 中同上命名規則的 method
但 class 的名字前綴為 Test* 且沒有 __init__ method - 特殊檔名
- conftest.py (可多個)
- pytest.ini (唯一)
- tox.ini
- setup.cfg
基本指令
- and, or, not 可任意組合
- -k EXPRESSION
- 指定要測試的 function or class
- pytest -k "show or close"
名字中擁有 show 或 close - -m MARKEXPR
- 指定要測試的 marker
- decorator:@pytest.mark.*
例:@pytest.mark.mark1 - pytest -m 'mark1 and not mark2'
標記 mark1 但不為 mark2 - -x, --exitfirst
- 一出錯就停止
- --maxfail=num
- 最大容錯數目
- --capture=method
- 可用 method:fd | sys | no
pytest --capture=no # disable all capturing pytest --capture=sys # replace sys.stdout/stderr with in-mem files pytest --capture=fd # also point filedescriptors 1 and 2 to temp file
- -s
- 等同上面的 --capture=no.
- --lf, --last-failed
- 只重跑上次失敗的測試,若上次無失敗,則重跑全部
- --ff, --failed-first
- 先跑上次失敗的,再接著跑剩下的測試
- -v, --verbose
- 印出詳細訊息
- -q, --quiet
- 只印出必要訊息
- -l, --showlocals
- 印出 tracebacks 的 local 變數
- --tb=style
- traceback 的印出模式
- auto:等同 long,但只印出第一筆跟最後一筆錯誤
- long:詳細模式
- short:簡單模式
- line:單行模式
- native:python 原本的錯誤訊息
- no:不印出
- --durations=N
- 前 N 名最慢的測試,也可以用來估計效率
- N=0 表示全部
- --collect-only
- 只收集測試並印出,但不執行
可用來確認指令是否正確,像是 -k - --version
- 目前 pytest 的版本資訊
- -r chars
- 印出額外資訊,可用來印出 skip 和 xfail 原因
- pytest -rsx
- (f) ailed
- (E) error
- (s) skipped
- (x) failed
- (X) passed
- (p) passed
- (P) passed with output
- (a) all except pP
- --disable-warnings, --disable-pytest-warnings
- 不印出 warnings
- --setup-show
- 印出 fixture 的過程
- --fixtures
- 印出使用的 fixture
- -h, --help
- help 文件
輸出格式
# test_show.py import pytest def test_sample1(): assert 1 == 1 def test_sample2(): assert [1, 2, 3] == [3, 2, 1] @pytest.mark.xfail() def test_sample3(): assert 1 != 1 @pytest.mark.xfail() def test_sample4(): assert 1 == 1建立好上面的檔案,輸入以下指令
pytest test_show.py首先,第一行表示 session 啟動,再來是基本訊息,並收集到四個測試
============================= test session starts ============================= platform win32 -- Python 3.7.0, pytest-4.0.2, py-1.7.0, pluggy-0.8.0 rootdir: C:\Users\zps\Desktop\pytest, inifile: collected 4 items再來是錯誤訊息,共四個測試,「點」表示 Pass,F 表示 Fail,x 表示預計是 fail,X 表示是預計是 Fail 卻 Pass
- PASSED (.): 測試成功
- FAILED (F): 測試失敗 (XPASS + strict 也視為失敗)
- SKIPPED (s): 跳過測試
- xfail (x): 指定失敗正常
- XPASS (X): 指定失敗正常,卻成功
- ERROR (E): 測試函數以外的錯誤,像是 fixture
test_show.py .FxX [100%] [100%]再來是錯誤的地方
================================== FAILURES =================================== ________________________________ test_sample2 _________________________________ def test_sample2(): > assert [1, 2, 3] == [3, 2, 1] E assert [1, 2, 3] == [3, 2, 1] E At index 0 diff: 1 != 3 E Use -v to get the full diff test_show.py:9: AssertionError最後的總結
========== 1 failed, 1 passed, 1 xfailed, 1 xpassed in 0.08 seconds ===========若要較詳細的資訊,可加上 -v
pytest test_show.py -v會直接說明測試的狀態,甚至會協助 debug
============================= test session starts ============================= platform win32 -- Python 3.7.0, pytest-4.0.2, py-1.7.0, pluggy-0.8.0 -- d:\coding\venv\devlop\scripts\python.exe cachedir: .pytest_cache rootdir: C:\Users\zps\Desktop\pytest, inifile: collected 4 items test_show.py::test_sample1 PASSED [ 25%] test_show.py::test_sample2 FAILED [ 50%] test_show.py::test_sample3 xfail [ 75%] test_show.py::test_sample4 XPASS [100%] ================================== FAILURES =================================== ________________________________ test_sample2 _________________________________ def test_sample2(): > assert [1, 2, 3] == [3, 2, 1] E assert [1, 2, 3] == [3, 2, 1] E At index 0 diff: 1 != 3 E Full diff: E - [1, 2, 3] E ? ^ ^ E + [3, 2, 1] E ? ^ ^ test_show.py:9: AssertionError ========== 1 failed, 1 passed, 1 xfailed, 1 xpassed in 0.05 seconds ===========
subset Test
- 名字皆可來自於 -v,所見即所得
- 測試整個資料夾
pytest folderName
- 測試單個檔案
pytest fileName
- 測試檔案中的特定 function,需完全一模一樣
pytest fileName::funcName
- 測試檔案中的特定 function 的特定輸入參數,需完全一模一樣
pytest fileName::funcName[parameter]
- 測試檔案中的特定 class,需完全一模一樣
pytest fileName::className
- 測試檔案中的特定 class 的 method,需完全一模一樣
pytest fileName::className::methodName
- 測試名字擁有特定字串的 function
pytest -k "condition1 and codition2 or not condition3"
Test Function
- 一切建立在 assert 即可
def test_basic(): assert 1 == 1
- exception 測試
def test_exception(): with pytest.raises(TypeError): raise TypeError
- mark function
@pytest.mark.fruits def test_apple(): assert 1 == 1 @pytest.mark.fruits @pytest.mark.vegetable def test_tomato(): assert 1 == 1
可指定 mark 測試,加上 -m
pytest test_sample.py -m "fruits and not vegetable" -v
============================= test session starts ============================= platform win32 -- Python 3.7.0, pytest-4.0.2, py-1.7.0, pluggy-0.8.0 -- d:\coding\venv\devlop\scripts\python.exe cachedir: .pytest_cache rootdir: C:\Users\zps\Desktop\pytest, inifile: collected 34 items / 33 deselected test_sample.py::test_apple PASSED [100%] =================== 1 passed, 33 deselected in 0.11 seconds ===================
@pytest.mark.skip(reason="skip this") def test_skip(): assert False import sys @pytest.mark.skipif(sys.version_info < (4, 3), reason="requires python4.3") def test_skipif(): assert False加上 -r,輸入以下指令,可印出跳過的原因
pytest test_sample.py -k skip -rxs
============================= test session starts ============================= platform win32 -- Python 3.7.0, pytest-4.0.2, py-1.7.0, pluggy-0.8.0 rootdir: C:\Users\zps\Desktop\pytest, inifile: collected 34 items / 32 deselected test_sample.py ss [100%] =========================== short test summary info =========================== SKIP [1] test_sample.py:33: skip this SKIP [1] test_sample.py:41: requires python4.3 ================== 2 skipped, 32 deselected in 0.03 seconds ===================
也可設定條件,決定是否視為 xfail
當 xfail_strict=true 時,XPASS 也會視為 Fail,可在 pytest.ini 內設定
@pytest.mark.xfail(reason="fail but still test") def test_xfail(): assert False @pytest.mark.xfail(reason="fail but still test maybe pass") def test_xpass(): assert True version = (1, 0, 0) @pytest.mark.xfail(version < (0, 5, 0), reason="if version over 0.5.0, it should pass") def test_condition_xfail(): assert True加上 -r,輸入以下指令,可印出指定 fail 的原因
pytest test_sample.py -k _x -rxs
============================= test session starts ============================= platform win32 -- Python 3.7.0, pytest-4.0.2, py-1.7.0, pluggy-0.8.0 rootdir: C:\Users\zps\Desktop\pytest, inifile: collected 34 items / 31 deselected test_sample.py xX. [100%] =========================== short test summary info =========================== XFAIL test_sample.py::test_xfail fail but still test ======== 1 passed, 31 deselected, 1 xfailed, 1 xpassed in 0.06 seconds ========
但在 ids_func 則是逐元素代入,這點要注意
@pytest.mark.parametrize("a,b,c", [(1, 2, 3), (4, 5, 6)]) def test_para(a, b, c): assert (a, b, c) == (1, 2, 3) @pytest.mark.parametrize("a,b,c", [(1, 2, 3), (4, 5, 6)], ids=["tuple1", "tuple2"]) def test_para_ids(a, b, c): assert (a, b, c) == (1, 2, 3) def ids_func(t): return "[{}]".format(t) @pytest.mark.parametrize("a,b,c", [(1, 2, 3), (4, 5, 6)], ids=ids_func) def test_para_ids_func(a, b, c): assert (a, b, c) == (1, 2, 3) class Info: def __init__(self, a, b, c): self.a = a self.b = b self.c = c @pytest.mark.parametrize("i", [Info(1, 2, 3), Info(4, 5, 6)]) def test_para_class(i): assert i == Info(1, 2, 3) def ids_class_func(t): return "Info({},{},{})".format(t.a, t.b, t.c) @pytest.mark.parametrize("i", [Info(1, 2, 3), Info(4, 5, 6)], ids=ids_class_func) def test_para_ids_class_func(i): assert i == Info(1, 2, 3) @pytest.mark.parametrize( "i", [ pytest.param(Info(1, 2, 3), id="basic"), pytest.param(Info(4, 5, 6), id="advance"), ], ) def test_para_ids_setting(i): assert i == Info(1, 2, 3)
pytest test_sample.py -k test_para -v --tb=no
============================= test session starts ============================= platform win32 -- Python 3.7.0, pytest-4.0.2, py-1.7.0, pluggy-0.8.0 -- d:\coding\venv\devlop\scripts\python.exe cachedir: .pytest_cache rootdir: C:\Users\zps\Desktop\pytest, inifile: collected 34 items / 22 deselected test_sample.py::test_para[1-2-3] PASSED [ 8%] test_sample.py::test_para[4-5-6] FAILED [ 16%] test_sample.py::test_para_ids[tuple1] PASSED [ 25%] test_sample.py::test_para_ids[tuple2] FAILED [ 33%] test_sample.py::test_para_ids_func[[1]-[2]-[3]] PASSED [ 41%] test_sample.py::test_para_ids_func[[4]-[5]-[6]] FAILED [ 50%] test_sample.py::test_para_class[i0] FAILED [ 58%] test_sample.py::test_para_class[i1] FAILED [ 66%] test_sample.py::test_para_ids_class_func[Info(1,2,3)] FAILED [ 75%] test_sample.py::test_para_ids_class_func[Info(4,5,6)] FAILED [ 83%] test_sample.py::test_para_ids_setting[basic] FAILED [ 91%] test_sample.py::test_para_ids_setting[advance] FAILED [100%] ============== 9 failed, 3 passed, 22 deselected in 0.14 seconds ==============
Fixture
- 可獨立放在 confest.py 中
- fixture 基本運用,fixture 本身的名字即是參數名
@pytest.fixture def fixture_fun1(): return 1 @pytest.fixture def fixture_fun2(): print("\nsetup") yield 1 print("\nteardown") def test_fixture(fixture_fun1, fixture_fun2): assert fixture_fun1 == 1 assert fixture_fun2 == 1
輸入以下指令
pytest test_sample.py::test_fixture -v -s
================================================================== test session starts =================================================================== platform win32 -- Python 3.7.0, pytest-4.0.2, py-1.7.0, pluggy-0.8.0 -- d:\coding\venv\devlop\scripts\python.exe cachedir: .pytest_cache rootdir: C:\Users\zps\Desktop\pytest, inifile: collected 1 item test_sample.py::test_fixture setup PASSED teardown ================================================================ 1 passed in 0.03 seconds ================================================================
- scope 展現,未指定 scope 預設為 function
因 class 無法輸入參數,所以會使用 @pytest.mark.usefixtures
其中的 method 仍可以輸入 fixture
@pytest.fixture(scope="function") def func_scope(): pass @pytest.fixture(scope="module") def mod_scope(): pass @pytest.fixture(scope="session") def sess_scope(): pass @pytest.fixture(scope="class") def class_scope(): pass def test_fixture_scope1(sess_scope, mod_scope, func_scope): pass def test_fixture_scope2(sess_scope, mod_scope, func_scope): pass @pytest.mark.usefixtures("class_scope") class TestClassFixtures: def test_method1(self, mod_scope): pass def test_method2(self, func_scope): pass
輸入以下指令
pytest test_sample.py -k "test_fixture_scope or TestClass" --setup-show
============================= test session starts ============================= platform win32 -- Python 3.7.0, pytest-4.0.2, py-1.7.0, pluggy-0.8.0 rootdir: C:\Users\zps\Desktop\pytest, inifile: collected 34 items / 30 deselected test_sample.py SETUP S sess_scope SETUP M mod_scope SETUP F func_scope test_sample.py::test_fixture_scope1 (fixtures used: func_scope, mod_scope, sess_scope). TEARDOWN F func_scope SETUP F func_scope test_sample.py::test_fixture_scope2 (fixtures used: func_scope, mod_scope, sess_scope). TEARDOWN F func_scope SETUP C class_scope test_sample.py::TestClassFixtures::test_method1 (fixtures used: class_scope, mod_scope). SETUP F func_scope test_sample.py::TestClassFixtures::test_method2 (fixtures used: class_scope, func_scope). TEARDOWN F func_scope TEARDOWN C class_scope TEARDOWN M mod_scope TEARDOWN S sess_scope =================== 4 passed, 30 deselected in 0.05 seconds ===================
- fixture 可互相呼叫,但只能呼叫本身以上的 scope,session > module > class > function
@pytest.fixture def func_scope_with_sess_scope(sess_scope): pass def test_fixture_with_fixture(func_scope_with_sess_scope): pass # Error: ScopeMismatch @pytest.fixture(scope="session") def sess_scope_with_func_scope(func_scope): pass def test_fixture_with_wrong_fixture(sess_scope_with_func_scope): pass
- fixture 名字可重新命名
@pytest.fixture(name="shortName") def fixture_name_too_long(): pass def test_fixture_rename(shortName): pass
- 傳入參數,大致上跟 @pytest.mark.parametrize 差不多
但在 idsFunc 的處理上有些不同,在此並未逐元素傳入
request 則是內建的特別 fixtures
@pytest.fixture(params=[(1, 2, 3), [4, 5, 6]]) def func_fixture_para(request): return request.param def test_fixture_para(func_fixture_para): assert func_fixture_para == (1, 2, 3) @pytest.fixture(params=[(1, 2, 3), [4, 5, 6]], ids=["tuple1", "tuple2"]) def func_fixture_para_ids(request): return request.param def test_fixture_para_ids(func_fixture_para_ids): assert func_fixture_para_ids == (1, 2, 3) def ids_func_fixture(t): return "{},{},{}".format(t[0], t[1], t[2]) @pytest.fixture(params=[(1, 2, 3), [4, 5, 6]], ids=ids_func_fixture) def func_fixture_para_idsFunc(request): return request.param def test_fixture_para_ids_func(func_fixture_para_idsFunc): assert func_fixture_para_idsFunc == (1, 2, 3)
輸入以下指令
pytest test_sample.py -k test_fixture_para -v --tb=no
============================= test session starts ============================= platform win32 -- Python 3.7.0, pytest-4.0.2, py-1.7.0, pluggy-0.8.0 -- d:\coding\venv\devlop\scripts\python.exe cachedir: .pytest_cache rootdir: C:\Users\zps\Desktop\pytest, inifile: collected 35 items / 29 deselected test_sample.py::test_fixture_para[func_fixture_para0] PASSED [ 16%] test_sample.py::test_fixture_para[func_fixture_para1] FAILED [ 33%] test_sample.py::test_fixture_para_ids[tuple1] PASSED [ 50%] test_sample.py::test_fixture_para_ids[tuple2] FAILED [ 66%] test_sample.py::test_fixture_para_ids_func[1,2,3] PASSED [ 83%] test_sample.py::test_fixture_para_ids_func[4,5,6] FAILED [100%] ============== 3 failed, 3 passed, 29 deselected in 0.07 seconds ==============
Flask 範例
flask_pytest
├─src
│ ├─main.py
│ │
│ ├─flaskr
│ │ ├─main.py
│ │ ├─models.py
│ │ ├─views.py
│ │ └─__init__.py
│ │
│ └─instance
│ └config.cfg
│
└─tests
├─conftest.py
│
├─func
│ └test_views.py
│
└─unit
└test_models.py
詳細請參考連結的 source code
只列出比較重要的部分,首先必須定義 create_app function,這樣便可以自由載入設定檔
flaskr_pytest/src/flaskr/main.py
flaskr_pytest/tests/conftest.py
├─src
│ ├─main.py
│ │
│ ├─flaskr
│ │ ├─main.py
│ │ ├─models.py
│ │ ├─views.py
│ │ └─__init__.py
│ │
│ └─instance
│ └config.cfg
│
└─tests
├─conftest.py
│
├─func
│ └test_views.py
│
└─unit
└test_models.py
詳細請參考連結的 source code
只列出比較重要的部分,首先必須定義 create_app function,這樣便可以自由載入設定檔
flaskr_pytest/src/flaskr/main.py
""" main """ from flask import Flask, g from flask_sqlalchemy import SQLAlchemy ####################### #### Configuration #### ####################### # Create the instances of the Flask extensions (flask-sqlalchemy, flask-login, etc.) in # the global scope, but without any arguments passed in. These instances are not attached # to the application at this point. db = SQLAlchemy() ###################################### #### Application Factory Function #### ###################################### def create_app(config_filename=None): app = Flask(__name__, instance_relative_config=True) print("instance Path:", app.instance_path) app.config.from_pyfile(config_filename) initialize_extensions(app) register_blueprints(app) @app.before_request def configSetting(): g.USERNAME = app.config["USERNAME"] g.PASSWORD = app.config["PASSWORD"] print("DB Path:", app.config["SQLALCHEMY_DATABASE_URI"]) return app ########################## #### Helper Functions #### ########################## def initialize_extensions(app): # Since the application instance is now created, pass it to each Flask # extension instance to bind it to the Flask application instance (app) db.init_app(app) def register_blueprints(app): # Since the application instance is now created, register each Blueprint # with the Flask application instance (app) from .views import flaskr app.register_blueprint(flaskr)利用 fixture 達到初始化目的,再來就是自由發揮了
flaskr_pytest/tests/conftest.py
import pytest from flask import g, session import sys, os # 若是用 pip install -e,無需下面兩行 myPath = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, myPath + "/../src") from flaskr import create_app, db, models @pytest.fixture def new_User(test_client): user = models.User(name="Sun") return user @pytest.fixture(scope="session") def test_client(): app = create_app("config.cfg") @app.before_request def configSetting(): g.USERNAME = app.config["USERNAME"] g.PASSWORD = app.config["PASSWORD"] # Flask provides a way to test your application by exposing the Werkzeug test Client # and handling the context locals for you. client = app.test_client() # Establish an application context before running the tests. # for SQLAlchemy setting ctx = app.app_context() ctx.push() yield client # this is where the testing happens! ctx.pop() @pytest.fixture def init_db(test_client): # Create the database and the database table db.create_all() yield db db.session.remove() db.drop_all()
參考
Testing Flask ApplicationsTesting a Flask Application using pytest
Testing Flask SQLAlchemy database with pytest
Python Testing with pytest
留言
張貼留言