pytestで一時ファイルが必要なテストを行う
tempfile × pytestを組み合わせる
pytestでテストを書く際、以下の状況に遭遇したことはありますか。
- 一時的にファイルを作成したい
- テスト終了後にファイルを削除したい
代表は外部ファイルを読み込んで処理を行うコードだと思います。
そんな時はtempfile
を使うといい感じにテストすることができます。
ディレクトリ構成
.
├── csv_read.py
└── tests
├── __init__.py
└── test_csv_read.py
- csv_read.py 今回のテスト対象
- test_csv_read.py テストコードモジュール
使用ライブラリ
以下を使用しました。
- pytest
- pydantic(テスト対象のクラスに使用)
テストを行うクラス
今回は、CSVを読み込む事を想定したクラスを準備しました。
この内、インスタンス化する際のバリデーションチェックをテストします。
from pathlib import Path
from pydantic import BaseModel, Field, validator
class CSVReader(BaseModel):
"""CSV読み込みクラス"""
path: Path = Field(..., description="CSVパス")
@validator("path")
def validate_path(cls, path: Path) -> Path:
"""パス検査
Args:
path (Path): CSVパス
Raises:
ValueError: 検証失敗
Returns:
Path: CSVパス
"""
# ファイル存在検証
if not path.exists():
raise ValueError(f"ファイルが存在しません:{path}")
# 拡張子検証
if path.suffix != ".csv":
raise ValueError(f"拡張子に誤りがあります:{path.name}")
return path
pydantic
のvalidator
はデコレートにすることで
インスタンス化前にバリデーションチェックを行うことができます。
上記では2点チェックを行っています。
- ファイルが存在しているか
- 拡張子が
.csv
か
これらをテストすることにします。
テスト実装
先程のクラスをtempfile
でテストします。
tempfileについて
実装前にtempfile
について簡単に解説します。
tempfile
は読んで字の如く、一時ファイルに関する操作を行う標準パッケージです。
詳しい使い方はこちらの記事が参考になります。
NamedTemporaryFile
を使うと、一時ファイルが作成されたパスを取得できます。
suffix
パラメータに拡張子を指定すると、任意の拡張子ファイルを作成できます。
import tempfile
# /tmp/{tempfile_name}.csv
print(tempfile.NamedTemporaryFile(suffix=".csv", delete=True).name)
テストコード
では、テストを実装します。
- ファイルが存在しない
- 拡張子が誤っている
- 検証通過
の3パターンを網羅しました。
import tempfile
from pathlib import Path
from typing import Any, Generator
import pytest
from pydantic import ValidationError
from csv_read import CSVReader
@pytest.fixture(scope="function")
def json_file() -> Generator[Path, Any, None]:
"""json一時ファイル作成
Yields:
Generator[Path, Any, None]: json一時ファイルパス
"""
path = Path(tempfile.NamedTemporaryFile(suffix=".json", delete=False).name)
yield path
# テスト終了後にファイルを削除
path.unlink()
@pytest.fixture(scope="function")
def csv_file() -> Generator[Path, Any, None]:
"""csv一時ファイル作成
Yields:
Generator[Path, Any, None]: csv一時ファイルパス
"""
path = Path(tempfile.NamedTemporaryFile(suffix=".csv", delete=False).name)
yield path
# テスト終了後にファイルを削除
path.unlink()
def test_not_exists_path() -> None:
"""バリデーションチェックエラー
ファイルが存在しない
"""
path = Path(tempfile.NamedTemporaryFile(suffix=".csv", delete=True).name)
with pytest.raises(ValidationError) as e:
_ = CSVReader(path=path)
msg = e.value.errors()[0]["msg"]
assert msg == f"ファイルが存在しません:{path}"
def test_not_suffix_csv(json_file: Path) -> None:
"""バリデーションエラー
拡張子不正
Args:
json_file (Path): json一時ファイルパス
"""
with pytest.raises(ValidationError) as e:
_ = CSVReader(path=json_file)
msg = e.value.errors()[0]["msg"]
assert msg == f"拡張子に誤りがあります:{json_file.name}"
def test_validate_ok(csv_file: Path) -> None:
"""バリデーションチェック通過
Args:
csv_file (Path): csv一時ファイルパス
"""
reader = CSVReader(path=csv_file)
assert reader.path == csv_file
ポイントをいくつかご紹介します。
一時ファイルの作成と削除
pytest.fixture
を使って
- 作成を行う前処理
- 削除を行う後処理
を実現しています。
@pytest.fixture(scope="function")
def json_file() -> Generator[Path, Any, None]:
"""json一時ファイル作成
Yields:
Generator[Path, Any, None]: json一時ファイルパス
"""
path = Path(tempfile.NamedTemporaryFile(suffix=".json", delete=False).name)
# テスト関数にパスを返却
yield path
# テスト終了後にファイルを削除
path.unlink()
NamedTemporaryFile().name
でパスを取得した後、pathlib.Path
でパスのインスタンス化を行っています。
delete=False
パラメータをつけることで、一時ファイルを削除せずに保持することが可能です。
pathlib
はパス関係の処理を盛り込んでいる標準パッケージです。
パスをオブジェクト感覚で操作出来るため、パスの処理をしたいなら是非使用してみてください。
yield
で本処理にパスを返却します。
テスト終了後はpath.unlink()
で自身のファイルを削除して、後処理を完了させます。
バリデーションエラーを拾う
pytest.raises()
で拾います。
def test_not_exists_path() -> None:
"""バリデーションチェックエラー
ファイルが存在しない
"""
path = Path(tempfile.NamedTemporaryFile(suffix=".csv", delete=True).name)
with pytest.raises(ValidationError) as e:
_ = CSVReader(path=path)
msg = e.value.errors()[0]["msg"]
assert msg == f"ファイルが存在しません:{path}"
pydantic.ValidationError
でエラー受けし、メッセージの確認をしています。
まとめ
pytestを使って外部ファイルが必要なテストについて紹介しました。
- tempfileで一時ファイルを作成する
- pytest.fixtureで前処理と後処理を実現する
これらを活用すれば、一時的なファイルの作成~削除までをこなすことができます。
是非試してみてください。
追記
執筆中、pytestにtmp_pathという機能があることを知りました。
使えそうであれば記事に追記しておきます。
Discussion