1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

VS CodeのTest Explorerではpytestが成功するのに、ターミナルから実行すると失敗することがありました。

python -m pytest

結果は次のようなエラーです。

E   KeyError: 'API_BASE_URL'

ERROR tests/test_config.py - KeyError: 'API_BASE_URL'
Interrupted: 1 error during collection

原因は、VS CodeのTest Explorerとターミナルで、.envの扱いが異なっていたことでした。

ただし、単にVS Codeの設定を揃えるだけでは、CI環境で同じ問題が再発します。

今回は、設定による解決と、環境に依存しないテストへの修正を行います。

再現用コード

ディレクトリ構成は次のとおりです。

.
├── .env
├── app
│   └── config.py
└── tests
    └── test_config.py

.envにはAPIの接続先を定義します。

.env
API_BASE_URL=https://example.test

app/config.pyでは、環境変数を読み込みます。

app/config.py
import os

API_BASE_URL = os.environ["API_BASE_URL"]

テストは次のようにします。

tests/test_config.py
from app.config import API_BASE_URL


def test_api_base_url() -> None:
    assert API_BASE_URL == "https://example.test"

VS CodeのTest Explorerでは成功しました。

1 passed

しかし、ターミナルでは失敗しました。

python -m pytest -q
ERROR collecting tests/test_config.py
E   KeyError: 'API_BASE_URL'

1 error during collection

テスト関数が失敗したのではなく、テストの収集中にエラーが発生しています。

なぜTest Explorerだけ成功したのか

VS CodeのPython拡張機能には、環境変数ファイルを指定する設定があります。

.vscode/settings.json
{
    "python.envFile": "${workspaceFolder}/.env",
    "python.testing.pytestEnabled": true,
    "python.testing.pytestArgs": [
        "tests"
    ]
}

python.envFileのデフォルト値は、次のパスです。

${workspaceFolder}/.env

Python拡張機能は、このファイルの環境変数をテストの検出・実行時に読み込みます。

一方、VS Codeの統合ターミナルへ.envの内容を注入する設定は、別に存在します。

{
    "python.terminal.useEnvFile": true
}

この設定のデフォルト値はfalseです。

つまり、初期設定では次のような差が生まれます。

Test Explorer
    → .envを読み込む

統合ターミナル
    → .envを自動では読み込まない

そのため、同じVS Code上でも実行方法によって結果が変わります。

ターミナル側の設定を揃える

Test Explorerと統合ターミナルの環境を揃えるには、次のように設定します。

.vscode/settings.json
{
    "python.envFile": "${workspaceFolder}/.env",
    "python.terminal.useEnvFile": true,
    "python.testing.pytestEnabled": true,
    "python.testing.pytestArgs": [
        "tests"
    ]
}

設定後、新しいターミナルを開きます。

echo $API_BASE_URL
https://example.test

pytestも成功します。

python -m pytest -q
1 passed

ただし、これで問題が完全に解決したわけではありません。

import時に環境変数を読む問題

元のコードでは、モジュールのimport時に環境変数を読み込んでいます。

API_BASE_URL = os.environ["API_BASE_URL"]

pytestは、テストを収集するためにテストモジュールと、その中で参照されるモジュールをimportします。

そのため、環境変数が存在しない場合、次の順序で失敗します。

pytestを起動
    ↓
test_config.pyを収集
    ↓
app.configをimport
    ↓
API_BASE_URLを読み込む
    ↓
KeyError
    ↓
テスト関数まで到達しない

テスト関数の中で環境変数を設定しようとしても、importが先に実行されるため間に合いません。

def test_api_base_url(monkeypatch):
    monkeypatch.setenv("API_BASE_URL", "https://example.test")

    # この前にapp.configのimportが失敗している

環境変数を必要なときに読む

環境変数をモジュールのimport時ではなく、関数が呼ばれたときに読むように変更します。

app/config.py
import os


def get_api_base_url() -> str:
    try:
        return os.environ["API_BASE_URL"]
    except KeyError as exc:
        raise RuntimeError(
            "API_BASE_URL is required"
        ) from exc

これなら、app.configをimportしただけでは環境変数を読みません。

monkeypatchで環境依存をなくす

テストでは、pytestのmonkeypatchを使って環境変数を明示します。

tests/test_config.py
import pytest

from app.config import get_api_base_url


def test_get_api_base_url(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv(
        "API_BASE_URL",
        "https://example.test",
    )

    assert get_api_base_url() == "https://example.test"

環境変数が存在しない場合もテストします。

def test_get_api_base_url_requires_environment_variable(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.delenv(
        "API_BASE_URL",
        raising=False,
    )

    with pytest.raises(
        RuntimeError,
        match="API_BASE_URL is required",
    ):
        get_api_base_url()

monkeypatch.setenv()とmonkeypatch.delenv()で変更した環境変数は、各テストの終了後に元へ戻されます。

実行結果は次のとおりです。

python -m pytest -q
..                                       [100%]
2 passed

このテストは、.envが存在しなくても成功します。

mv .env .env.backup
python -m pytest -q
..                                       [100%]
2 passed

VS Code、通常のターミナル、CIのどこで実行しても、同じ条件でテストできるようになりました。

.envは実行用、monkeypatchはテスト用

今回の整理は次のとおりです。

.env
    → ローカルでアプリを実行するための設定

python.envFile
    → VS CodeのPython拡張機能が読むファイル

python.terminal.useEnvFile
    → 統合ターミナルにも.envを注入する設定

monkeypatch
    → テスト条件をコード内で固定する仕組み

.envをテストの前提にすると、開発者ごとの環境やCI設定によって結果が変わります。

単体テストで必要な値は、可能な限りテストコード側で明示した方が再現性を高められます。

.envをGitへ追加しない

APIキーなどを含む.envは、通常Gitへコミットしません。

.gitignore
.env

代わりに、変数名だけを書いた.env.exampleを置きます。

.env.example
API_BASE_URL=

これで、必要な環境変数を共有しつつ、実際の値は各環境で管理できます。

まとめ

VS CodeのTest Explorerだけpytestが成功するときは、テストコード以外に.envの読み込み差も確認する必要があります。

{
    "python.envFile": "${workspaceFolder}/.env",
    "python.terminal.useEnvFile": true
}

ただし、設定を揃えるだけではCIとの環境差は残ります。

より重要だったのは、次の2点です。

  • 環境変数をimport時に固定しない
  • テスト条件はmonkeypatchで明示する

Test Explorerでテストが通ることだけでなく、どこで実行しても同じ結果になることが、テストの再現性では重要だと感じました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?