当サイトは、アフィリエイト広告を利用しています
VSCodeでpytestを
のテスト実行環境別に実行する方法を調べたのでまとめておく。
当記事では
についてまとめる。
なお、テストの実行方法のみを解説するため
テスト内容等は今回は割愛する
pytestは CUI(Character User Interface)ツールで、
コマンドライン上で実行するテストフレームワークであり、
対話的なGUIやグラフィカルなユーザーインターフェースを提供していない。
そのため通常テストコードを書いた後、ターミナルから
コマンド実行で実施する。
コマンドで実行する場合は下記のようになる。
サンプルテストコード
from unittest.mock import Mockimport pytestfrom main import create_appdef test_1():mock = Mock()def test_2():mock = Mock()def test_3():mock = Mock()def test_4():mock = Mock()def test_5():mock = Mock()def test_6():mock = Mock()
ターミナルからコマンド実行する
rootdir: C:\develop\01_TechSamples\Python\FastApi\fastApi_pytestplugins: anyio-4.6.2.post1, cov-5.0.0collected 6 itemstests/test_main.py::test_1 PASSED [ 16%]tests/test_main.py::test_2 PASSED [ 33%]tests/test_main.py::test_3 PASSED [ 50%]tests/test_main.py::test_4 PASSED [ 66%]tests/test_main.py::test_5 PASSED [ 83%]tests/test_main.py::test_6 PASSED [100%]======================== 6 passed in 0.96s ========================
このようにpytestは通常はテストコードを作成後、ターミナルから
コマンドで実行する。
テストコードを書いた後はコマンド実行で問題ないが
テストコード作成中にデバッグで動作を確認することはできない。
先にイメージだけ見せると
VScodeからpytest実行する場合は下記のようになる
コマンドではなくVScode上でのpytestをするメリットをするをまとめる
VSCodeを使うことでグラフィカルなユーザーインターフェースで実行することができる
チェックマークを右クリックすることでテストケースを個別で実行できる
の任意の単位でテストを実行できる
テストケース実行でブレークポイントを打って
デバッグ実行を行えばデバッグできる
実際にFastApiで作ったサンプルREST APIのテストをVSCodeから実行してみる
テストの実行環境によって行う設定が異なるので
テストの実行環境が
の場合別で実行する。
まずはローカルでpytestを実行する場合の設定方法を解説する。
テスト対象のFastApi製REST APIのプロジェクト構成としては
下記のようにする
.|-- .vscode| |-- .env| |-- launch.json| `-- settings.json|-- app| |-- __init__.py| |-- asgi.py| |-- endpoints.py| `-- main.py|-- requirements.txt`-- tests|-- test_asgi.py|-- test_endpoints.py`-- test_main.py
fastapiuvicorn[standard]pytestpytest-asyncio
必要なパッケージをインストールする
REST API自体のモジュール
pytestのテスト対象となるモジュール
from main import create_appapp = create_app()
FastApiアプリケーションをASGIサーバーで起動するための エントリーポイントとなるモジュール
from fastapi import APIRouterfrom fastapi.responses import JSONResponserouter = APIRouter()# ディクショナリリストusers = [{"user_id": "1", "name": "Tujimura", "age": 11},{"user_id": "2", "name": "mori", "age": 20},{"user_id": "3", "name": "shimada", "age": 50},{"user_id": "4", "name": "kyogoku", "age": 70}]# ディクショナリの一覧を取得@router.get("/")async def get_users():return JSONResponse(status_code=200, content=users)
エンドポイントを管理するモジュール
from fastapi import FastAPIimport endpoints as endpointsdef create_app():# アプリケーションインスタンスの作成app = FastAPI()# ルーティング設定app.include_router(endpoints.router)return app
アプリケーションの初期化処理を行うモジュール。
テスト対象モジュールのテストを行うモジュール
テスト対象モジュールと1:1で作成する
pytestでテストモジュールを作成する場合
「test_」で始まる名前にする必要がある
from unittest.mock import Mockimport importlibimport asgidef test_asgi(monkeypatch):# create_app()が実行された場合にmock_appが返るように置き換えmock_app = Mock()monkeypatch.setattr("main.create_app", mock_app)# asgiモジュールを再インポートして評価させるimportlib.reload(asgi)# create_app関数(mock)が呼び出されたか確認mock_app.assert_called_once()
from unittest.mock import Mockfrom main import create_appimport endpointsdef test_create_app(monkeypatch):# Mockの FastAPIインスタンスを作成mock_app_instance = Mock()# include_routerメソッドを設定mock_app_instance.include_router.return_value = True# FastAPI()が実行された場合にmock_app_instanceが返るように置き換えmock_app = Mock(return_value=mock_app_instance)monkeypatch.setattr("main.FastAPI", mock_app)# create_app 関数を実行create_app()# include_router が endpoints.router を引数に呼び出されたか確認mock_app_instance.include_router.assert_called_with(endpoints.router)
from unittest.mock import Mockfrom main import create_appimport endpointsdef test_create_app(monkeypatch):# Mockの FastAPIインスタンスを作成mock_app_instance = Mock()# include_routerメソッドを設定mock_app_instance.include_router.return_value = True# FastAPI()が実行された場合にmock_app_instanceが返るように置き換えmock_app = Mock(return_value=mock_app_instance)monkeypatch.setattr("main.FastAPI", mock_app)# create_app 関数を実行create_app()# include_router が endpoints.router を引数に呼び出されたか確認mock_app_instance.include_router.assert_called_with(endpoints.router)
VScodeでpytestを行うための設定ファイル群
PYTHONPATH=./app
PYTHONPATHを指定する。
VSCodeは.envの./はそのファイルがどこにあっても ルートディレクトリ、つまり${workspaceFolder}として認識する。
コマンドラインの場合、./はカレントディレクトリが基準になるので注意
pytestをVSCode上で実行する際、カレントディレクトリは
になる。
その場合、「PYTHONPATH=./app」の指定がないと
テスト対象モジュールの
from fastapi import FastAPIimport endpoints as endpointsdef create_app():# アプリケーションインスタンスの作成app = FastAPI()# ルーティング設定app.include_router(endpoints.router)return app
やテストモジュールの
from unittest.mock import Mockfrom main import create_appimport endpointsdef test_create_app(monkeypatch):# Mockの FastAPIインスタンスを作成mock_app_instance = Mock()# include_routerメソッドを設定mock_app_instance.include_router.return_value = Trueprint("dddd")# FastAPI()が実行された場合にmock_app_instanceが返るように置き換えmock_app = Mock(return_value=mock_app_instance)monkeypatch.setattr("main.FastAPI", mock_app)# create_app 関数を実行create_app()# include_router が endpoints.router を引数に呼び出されたか確認mock_app_instance.include_router.assert_called_with(endpoints.router)
でimportしているmainモジュールやendpointsモジュールを
pythonは発見できないため。
pythonがimportするモジュールを
の順で探すが、カレントディレクトリである${workspaceFolder}には
mainモジュールやendpointsモジュールはないため、見つからない。
※その中のサブディレクトリ(例えば ${workspaceFolder}/app)までは自動で探索してくれない
そこでPYTHONPATHに「PYTHONPATH=./app」を
追加してmainモジュールやendpointsモジュールのある
「${workspaceFolder}/app」を検索パスに入れることで探索可能にしている。
{// PYTHONPATHの設定"python.envFile": "${workspaceFolder}/.vscode/.env",// pytestを実行する際の引数を設定"python.testing.pytestArgs": ["${workspaceFolder}", //--rootdir テスト実行のルートディレクトリを指定"--cov=${workspaceFolder}", //カバレッジ計測を指定"-v", // 詳細モード有効化"-s", // printやlogをターミナル表示],"python.testing.unittestEnabled": false,"python.testing.pytestEnabled": true}
VScodeで設定ファイルでテスト実行環境に関する設定をする
ここでVSCodeに「/.vscode/.env」を読み込んで環境変数(PYTHONPATH)を設定している。
pytest実行時のコマンドライン引数を指定する
unittest フレームワークを無効化
pytest フレームワークを有効化
VScodeのデバッグ設定ファイル
{"version": "0.2.0","configurations": [{"name": "Python: Current File","type": "debugpy","request": "launch","program": "${file}","purpose": ["debug-test"],"console": "integratedTerminal","env": {"PYTEST_ADDOPTS": "--no-cov"}}]}
settings.jsonで「--cov=${workspaceFolder}」を指定している場合
このファイルがないとデバッグでブレークポイントが効かなくなる
設定ができたらローカルでpytestを実行する
実行方法は
の2つの実行方法がある。
VScode上でグラフィカルに実行する
VScode上でpytestを実行するのに必要な拡張機能をインストールする
pytestはデフォルトでカレントディレクトリ直下からtestsという名前のディレクトリや
test_ で始まる名前のファイルを探して実行する。
ルート直下のtetsディレクトリあるためルートを選択する
上記手順を行った場合、.vscode/settings.jsonが
{// PYTHONPATHの設定"python.envFile": "${workspaceFolder}/.vscode/.env",// pytestを実行する際の引数を設定"python.testing.pytestArgs": ["."],"python.testing.unittestEnabled": false,"python.testing.pytestEnabled": true}
のように一部初期化されるので注意
当記事冒頭で説明したようにVScode上でテストを実行できる
VScodeのターミナルからコマンド実行する
VScodeのターミナルからコマンド実行すると
$ pytest -vC:\Users\lunaj\AppData\Local\Programs\Python\Python311\Lib\site-packages\pytest_asyncio\plugin.py:208: PytestDeprecationWarning: The configuration option "asyncio_default_fixture_loop_scope" is unset.The event loop scope for asynchronous fixtures will default to the fixture caching scope. Future versions of pytest-asyncio will default the loop scope for asynchronous fixtures to function scope. Set the default fixture loop scope explicitly in order to avoid unexpected behavior in the future. Valid fixture loop scopes are: "function", "class", "module", "package", "session"warnings.warn(PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET))========================================================================= test session starts =========================================================================platform win32 -- Python 3.11.1, pytest-8.2.2, pluggy-1.5.0 -- C:\Users\lunaj\AppData\Local\Programs\Python\Python311\python.execachedir: .pytest_cacherootdir: C:\develop\01_TechSamples\Python\FastApi\fastApi_pytestplugins: anyio-4.6.2.post1, asyncio-0.24.0, cov-5.0.0asyncio: mode=Mode.STRICT, default_loop_scope=Nonecollected 0 items / 3 errors=============================================================================== ERRORS ================================================================================_________________________________________________________________ ERROR collecting tests/test_asgi.py _________________________________________________________________ImportError while importing test module 'C:\develop\01_TechSamples\Python\FastApi\fastApi_pytest\tests\test_asgi.py'.Hint: make sure your test modules/packages have valid Python names.Traceback:C:\Users\lunaj\AppData\Local\Programs\Python\Python311\Lib\importlib\__init__.py:126: in import_modulereturn _bootstrap._gcd_import(name[level:], package, level)tests\test_asgi.py:3: in <module>import asgiE ModuleNotFoundError: No module named 'asgi'______________________________________________________________ ERROR collecting tests/test_endpoints.py _______________________________________________________________ImportError while importing test module 'C:\develop\01_TechSamples\Python\FastApi\fastApi_pytest\tests\test_endpoints.py'.Hint: make sure your test modules/packages have valid Python names.Traceback:C:\Users\lunaj\AppData\Local\Programs\Python\Python311\Lib\importlib\__init__.py:126: in import_modulereturn _bootstrap._gcd_import(name[level:], package, level)tests\test_endpoints.py:3: in <module>from endpoints import get_usersE ModuleNotFoundError: No module named 'endpoints'_________________________________________________________________ ERROR collecting tests/test_main.py _________________________________________________________________ImportError while importing test module 'C:\develop\01_TechSamples\Python\FastApi\fastApi_pytest\tests\test_main.py'.Hint: make sure your test modules/packages have valid Python names.Traceback:C:\Users\lunaj\AppData\Local\Programs\Python\Python311\Lib\importlib\__init__.py:126: in import_modulereturn _bootstrap._gcd_import(name[level:], package, level)tests\test_main.py:2: in <module>from main import create_appE ModuleNotFoundError: No module named 'main'======================================================================= short test summary info =======================================================================ERROR tests/test_asgi.pyERROR tests/test_endpoints.pyERROR tests/test_main.py!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 3 errors during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!========================================================================== 3 errors in 0.58s ==========================================================================
のようにエラーになる。
原因としてはでimportしているmainモジュールやendpointsモジュールを
pythonが発見できないため。
.vscode/.envでPYTHONPATH設定し、.vscode/setting.jsonで読み込ませているが
それはVScodeが認識しているのてあって、VSCode内でのエディタとデバッガに影響するが
ターミナルの環境には影響しない。ターミナルでの設定は、ターミナル自体で行う必要がある。
ターミナルでPYTHONPATHを設定してから実行する
# 環境変数でPYTHONPATHを設定$ export PYTHONPATH=$(pwd)/app# 環境変数の確認$ echo $PYTHONPATH/c/develop/01_TechSamples/Python/FastApi/fastApi_pytest/app# pytest実行$ pytest -vC:\Users\lunaj\AppData\Local\Programs\Python\Python311\Lib\site-packages\pytest_asyncio\plugin.py:208: PytestDeprecationWarning: The configuration option "asyncio_default_fixture_loop_scope" is unset.The event loop scope for asynchronous fixtures will default to the fixture caching scope. Future versions of pytest-asyncio will default the loop scope for asynchronous fixtures to function scope. Set the default fixture loop scope explicitly in order to avoid unexpected behavior in the future. Valid fixture loop scopes are: "function", "class", "module", "package", "session"warnings.warn(PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET))============================== test session starts ==================================platform win32 -- Python 3.11.1, pytest-8.2.2, pluggy-1.5.0 -- C:\Users\lunaj\AppData\Local\Programs\Python\Python311\python.execachedir: .pytest_cacherootdir: C:\develop\01_TechSamples\Python\FastApi\fastApi_pytestplugins: anyio-4.6.2.post1, asyncio-0.24.0, cov-5.0.0asyncio: mode=Mode.STRICT, default_loop_scope=Nonecollected 3 itemstests/test_asgi.py::test_asgi PASSED [ 33%]tests/test_endpoints.py::test_get_users_directly PASSED [ 66%]tests/test_main.py::test_create_app PASSED [100%]=============================== 3 passed in 1.68s ===================================
ターミナルでPYTHONPATHを設定してから実行すると成功する
次はpytestをdockerコンテナ上で実行する
pytestをコンテナで実行するメリットとしては下記がある
テスト対象のFastApi製REST APIのプロジェクト構成としては
下記のようにする
|-- .vscode# | |-- .env| |-- launch.json| `-- settings.json|-- app| |-- __init__.py| |-- asgi.py| |-- endpoints.py| `-- main.py|-- docker-compose.yml|-- dockerfile|-- requirements.txt`-- tests|-- test_asgi.py|-- test_endpoints.py`-- test_main.py
上記以外は変更はないので割愛する
dockerコンテナを作成するためのファイル群
FROM python:3.12# PYTHONPATHの設定ENV PYTHONPATH=/workspace/app# workspaceディレクトリ作成、移動WORKDIR /workspace# プロジェクトディレクトリにコピーCOPY requirements.txt /workspace# 必要パッケージのインストールRUN pip install --upgrade pipRUN pip install -r requirements.txt# uvicorn実行CMD ["uvicorn", "asgi:app", "--reload", "--host", "0.0.0.0", "--port", "8000"]
コンテナ内では、Dockerfileに
と設定すると、その環境変数がコンテナ内で実行されるすべてのプロセス(VSCodeのデバッガ、ターミナル、uvicorn サーバーなど)
で認識されるようになる。
つまりVScodeでもターミナルでもPYTHONPATHを認識する
同様にimportしているmainモジュールやendpointsモジュールを
pythonが発見できないためエラーとなる。
このDockerfileの場合、カレントディレクトリは/workspaceになるため
/workspace配下にないmainモジュールやendpointsモジュールはimportエラーとなる。
PYTHONPATHに/workspace/appを追加することでapp配下の
mainモジュールやendpointsモジュールをpythonが検索できるようになる
version: "3"services:fastapi:container_name: "fastapi"build:context: .dockerfile: Dockerfilevolumes:- .:/workspacetty: trueports:- 8000:8000 # ホストマシンのポート8000を、docker内のポート8000に接続する
VScodeでpytestを行うための設定ファイル群
{// // PYTHONPATHの設定// "python.envFile": "${workspaceFolder}/.vscode/.env",// pytestを実行する際の引数を設定"python.testing.pytestArgs": ["${workspaceFolder}", //--rootdir テスト実行のルートディレクトリを指定"--cov=${workspaceFolder}", //カバレッジ計測を指定"-v", // 詳細モード有効化"-s", // printやlogをターミナル表示],"python.testing.unittestEnabled": false,"python.testing.pytestEnabled": true}
VScodeで設定ファイルでテスト実行環境に関する設定をする
PYTHONPATHの設定は不要なので消す
そのほかは同じ
コンテナを作成してコンテナに接続する
# コンテナ作成$ docker-compose up -d# 確認$ docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES4a4b4b7d08a7 fastapi_pytest-fastapi "uvicorn asgi:app --…" 3 minutes ago Up 3 minutes 0.0.0.0:8000->8000/tcp fastapi
VScodeからコンテナに接続するには VSCodeの拡張機能である。
のどちらかを使用すれば、コンテナ内をVSCodeから操作できるので開発しやすい。
※この拡張機能はホスト側のVSCodeでインストールして使う
ちなみに自分はdocker-composeでコンテナを作った後
Docker(ms-azuretools.vscode-docker)でコンテナにアタッチしている
下記のような感じ
コンテナ接続後はローカルと同様に拡張機能
をインストールしておく
VScodeからコンテナに接続したら
でそれぞれ実行する
VSCodeでコンテナに接続後にVSCode上でpytestを実行する
VScodeにテストを検出させる方法はローカルと同様に
「F1のコマンドパレットでテストを構成する」から操作していく
※同じ手順
dockerfileでPYTHONPATHを指定しているため
.vscode/setting.jsonでのPYTHONPATHの指定は不要になる
VScodeで接続したコンテナのターミナルでpytestを実行する
root@4a4b4b7d08a7:/workspace# pytest -v/usr/local/lib/python3.12/site-packages/pytest_asyncio/plugin.py:208: PytestDeprecationWarning: The configuration option "asyncio_default_fixture_loop_scope" is unset.The event loop scope for asynchronous fixtures will default to the fixture caching scope. Future versions of pytest-asyncio will default the loop scope for asynchronous fixtures to function scope. Set the default fixture loop scope explicitly in order to avoid unexpected behavior in the future. Valid fixture loop scopes are: "function", "class", "module", "package", "session"warnings.warn(PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET))============================= test session starts =======================================platform linux -- Python 3.12.8, pytest-8.3.4, pluggy-1.5.0 -- /usr/local/bin/python3.12cachedir: .pytest_cacherootdir: /workspaceplugins: anyio-4.7.0, asyncio-0.24.0asyncio: mode=Mode.STRICT, default_loop_scope=Nonecollected 3 itemstests/test_asgi.py::test_asgi PASSED [ 33%]tests/test_endpoints.py::test_get_users_directly PASSED [ 66%]tests/test_main.py::test_create_app PASSED [100%]============================== 3 passed in 2.36s =========================================root@4a4b4b7d08a7:/workspace#
ターミナルでPYTHONPATHを設定しなくてもコマンド実行可能。
VScodeでのpytestの環境(ローカル、コンテナ)別実行方法の要点として
をまとめておく
ターミナルで 「export PYTHONPATH」を実行しても、VSCodeは認識しない: ターミナルとVSCodeのエディタは別々に動作しているため ターミナルで環境変数を設定しても、VSCodeの設定や開いているファイル、エディタに反映されない。
VSCodeに PYTHONPATH を認識させるには以下の設定が必要:
VSCodeで上記で PYTHONPATH を認識させてもターミナルでは認識しない:
Python はモジュールをインポートする際に、以下の順序でディレクトリを探索する
1,2において、その中のサブディレクトリまでは自動で探索しない。