python | 3.11.2 |
---|---|
OS | macOS Ventura 13.4.1 |
unittest
というユニットテスト用ライブラリやnose
という少し前までデファクトスタンダードだったユニットテストライブラリがありました。しかし、最近では更新されていないためpytest
を使うほうが主流になっています。unittest
やnose
の上位互換なユニットテストフレームワーク』と呼ばれています。unittest
やnose
と共存でき、移行が比較的簡単であることもあり、Pythonプロジェクトを抱える企業はpytest
に移行するケースも少なくありません。unittest
を拡張したものだが現在はメンテナンスされていないpytest
を使うことを推奨します。nose
やunittest
を使っている古いプロジェクトで作業している場合、それらを使い続けることは理にかなっているとも言えます。しかし、pytest
はそれらのフレームワークで書かれたテストを実行可能です。unittest
はPython組み込みのテストライブラリで、xUnitスタイルに従っています。標準ライブラリに含まれており、広い範囲のテストスイートを構築するための基盤として作用します。pytest
やnose
に比べて冗長assertEqual
やassertTrue
などの独自関数あり)nose
はunittest
を拡張し、テストの記述、検索、実行を容易にする外部ライブラリです。nosetests
コマンドで実行できます。unittest
よりも利用できるプラグインが多いunittest
よりも冗長性が低いpytest
はPythonの最も人気のあるテストフレームワークの1つです。よりモダンでPythonicなアプローチが特徴です。pytest
コマンドで実行できます。unittest
やnose
テストも実行可能$ pyenv virtualenv 3.11.2 pytest-sample
$ pyenv local pytest-sample
$ pip install pytest
$ pytest
======================================================== test session starts ========================================================
platform darwin -- Python 3.11.2, pytest-7.4.0, pluggy-1.2.0
rootdir: /pytest
collected 0 items
======================================================= no tests ran in 0.00s =======================================================
(pytest-sample)
test_xxx.py
やxxx_test.py
のようにファイル名にtest
をつけるとテストファイルとして認識されます。def say_hello():
return 'Hello, World!'
def test_say_hello():
assert say_hello() == 'Hello, World!'
$ pytest hello_test.py
======================================================== test session starts ========================================================
platform darwin -- Python 3.11.2, pytest-7.4.0, pluggy-1.2.0
rootdir: /pytest
collected 1 item
hello_test.py . [100%]
========================================================= 1 passed in 0.00s =========================================================
(pytest-sample)
def fail():
return 1
def test_fail():
assert fail() == 0
$ pytest fail_test.py
======================================================== test session starts ========================================================
platform darwin -- Python 3.11.2, pytest-7.4.0, pluggy-1.2.0
rootdir: /pytest
collected 1 item
fail_test.py F [100%]
============================================================= FAILURES ==============================================================
_____________________________________________________________ test_fail _____________________________________________________________
def test_fail():
> assert fail() == 0
E assert 1 == 0
E + where 1 = fail()
fail_test.py:5: AssertionError
====================================================== short test summary info ======================================================
FAILED fail_test.py::test_fail - assert 1 == 0
========================================================= 1 failed in 0.01s =========================================================
(pytest-sample)
pytest
は実行の際にpytest
やpython -m pytest
のようにすべてで実行するかファイル名を指定するかといった使い方ができます。さらに、オプションを与えることで出力をコントロールしたりデバッガを表示したりできます。オプション | 説明 |
---|---|
-h, --help | ヘルプの表示 |
-v, --verbose | キャッシュや成功結果など、細かな結果を表示 |
-q, --quiet | シンプルな失敗情報のみ表示 |
-k EXPRESSION | 与えた文字列にマッチするテストのみ実行 |
-x, --exitfirst | 最初にテストが失敗した箇所で停止 |
--maxfail=num | テスト失敗においてnum で指定した数で停止 |
-s, --capture=no | コード内のprint() を表示 |
-l, --showlocals | トレースバック内でローカル変数を表示 |
--ff, --failed-first | 失敗済みのテストを先に実行 |
--durations=num | num 個のテストを遅い順で表示 |
--cov=my_module | my_module のカバレッジを計測(要pytest-cov ) |
--cov-report=term | カバレッジレポートの種別(ターミナル、HTML, XMLなど)を指定(要pytest-cov ) |
-m MARKEXPR | マーカーを指定してテストを実行 |
--junitxml=path | 指定したパスにJUnit XML形式のレポートを作成 |
--setup-show | セットアップとティアダウンを表示 |
--doctest-modules | モジュール内にあるdoctestを実行 |
-r chars | chars で指定した形式で出力をカスタマイズ |
--collect-only | テストを集約して表示するが実行はしない |
--pdb | デバッガを出力 |
--lf, --last-failed | 最後に失敗したテストを実行 |
--tb=style | トレースバックを指定のstyle (short, long, nativeなど)で出力 |
def test_hello():
word = 'world'
assert word == 'hello', 'helloと言っていない'
FAILED test_hello.py::test_hello - AssertionError: helloと言っていない
pytest
は予約語であるassert
を覚えておけば基本的な使い方は可能です。assert
に続くコードがTrue
を返せば成功、Flase
を返せばテストは失敗します。pytest
独自のデコレータや関数を覚えておけば便利に使えます。ZeroDivisionError
が発生するようにします。def divide(a, b):
return a / b
from src.devide import devide
import pytest
def test_devide_by_zero():
with pytest.raises(ZeroDivisionError):
devide(1, 0)
with
でそのコンテキスト内に発生した例外をキャッチpytest.raises(ZeroDivisionError)
は、with
ブロック内でZeroDivisionError
が発生することを保証with
ブロック内のコードがZeroDivisionError
を発生させなければテスト失敗def test_divide_by_zero_message():
with pytest.raises(ZeroDivisionError, match="division by zero"):
divide(1, 0)
@pytest.fixture
def sample_data():
return {"key": "value"}
@pytest.mark.slow
def test_slow_function():
pass
@pytest.mark.skip(reason="スキップします")
def test_example():
pass
True
の場合にテストをスキップするようにマークします。@pytest.mark.skipif(sys.version_info < (3,7), reason="Python3.7以上のバージョンが必要です")
def test_example():
pass
@pytest.mark.parametrize("x, y, expected", [(1, 2, 3), (4, 5, 9)])
def test_add(x, y, expected):
assert x + y == expected
@pytest.mark.xfail
def test_example():
assert False
pytest
のディレクトリ構成はプロジェクトごとに合意が取れたものであればチーム内で共有しやすいですが、一例を提示しておきます。src/
ディレクトリへ実際のアプリケーションコードを入れ、tests/
にすべてのテストファイルを集約しています。.
├── README.md
├── requirements.txt
├── setup.py
├── src
│ └── mymodule
│ └── mymodule.py
└── tests
├── __init__.py
├── conftest.py
├── fixtures
├── integration
└── unit
└── mymodule
└── test_mymodule.py
-k
による実行するテストの指定以外にも、以下の方法で実行するテストを指定可能です。mymodule
にクラスを作成してみます。class MyModule(object):
def __init__(self):
pass
def add(self, a, b):
return a + b
from src.mymodule.mymodule import MyModule
def test_module():
mymodule = MyModule()
assert mymodule.add(1, 2) == 3
pytest tests/unit/mymodule/test_mymodule.py
pytest tests/unit/mymodule/test_mymodule.py::test_module
from src.mymodule.mymodule import MyModule
class TestMyModule(object):
def test_module():
mymodule = MyModule()
assert mymodule.add(1, 2) == 3
pytest tests/unit/mymodule/test_mymodule.py::TestMyModule
pytest tests/unit/mymodule/test_mymodule.py::TestMyModule::test_module
pytest
では、フィクスチャはセットアップアクションとティアダウン(またはポスト)アクションの両方を持つことができます。これらはそれぞれ、フィクスチャを使用するテスト関数の前と後に実行されるアクションです。yield
文の前に行うすべてのことで、ティアダウン(またはポストアクション)コードはyield
文の後に行うすべてのことです。import pytest
class SimpleDatabase:
def __init__(self):
self.connection = None
def connect(self):
# 接続状態
self.connection = "Connected"
def close(self):
# 接続終了状態
self.connection = None
@pytest.fixture
def database():
# セットアップ: データベース接続
db = SimpleDatabase()
db.connect()
yield db # テストにデータベースインスタンスを返す
# ティアダウン: データベース接続終了
db.close()
def test_database_connection(database):
assert database.connection == "Connected"
def test_database_func(database):
pass
# テスト終了後はティアダウンで接続が失われた状態
pytest
では、conftest.py
はディレクトリ内の複数のテストファイルで利用可能なフック、フィクスチャ、その他の設定を定義できます。主なメリットはコードの重複を避け、設定とフィクスチャのための一元的な設定データとして機能することです。conftest.py
の使い方は以下の通りです。conftest.py
をテストファイルがあるディレクトリ、または任意の親ディレクトリに設置(pytestは自動でconftest.py
を検知する)conftest.py
でフィクスチャを定義conftest.py
でフックを使うimport pytest
# 共有するフィクスチャの定義
@pytest.fixture
def database():
db = connect_to_database()
yield db
db.close()
# フックの定義
def pytest_configure(config):
config.addinivalue_line("markers", "slow: marks tests as slow")
# カスタムオプションの定義
def pytest_addoption(parser):
parser.addoption("--myoption", action="store", default="default_value", help="カスタムオプション")
@pytest.fixture
def myoption(request):
return request.config.getoption("--myoption")
# テストデータの共有
@pytest.fixture
def common_data():
return {"key": "value"}
def test_database_func(database):
pass
def test_myoption_receive(myoption):
assert myoption == "test"
pytest
を実行できます。$ pytest --myoption=test
pytest
では、テストの一般的な操作を支援するヘルパー関数を作成し、コードの再利用性と可読性を高めることができます。デフォルトでは組み込みのヘルパーを持ちませんが、ユーティリティ関数を作成するための標準的なPythonのアプローチを利用するだけで簡単に作成できます。import random
import string
def random_string(length=10):
"""固定幅のランダム文字列を生成"""
letters = string.ascii_lowercase
return ''.join(random.choice(letters) for i in range(length))
def generate_user():
"""ランダムなユーザーデータ生成"""
return {
"username": random_string(8),
"password": random_string(12),
"email": f"{random_string(5)}@example.com"
}
from utils import generate_user
import pytest
def test_user_registration():
user = generate_user()
# このデータを使ってユーザー登録の実装をテストする
# ユーザー登録機能がTrueかどうかをassert
def test_user_login():
user = generate_user()
# ログインをテスト
utils.py
のように別のファイルとして設置してインポートして使うべしpytest
はその他の外部ライブラリを利用することでより便利に利用できます。pytest-ordering
を使うと、テストの順番を制御可能です。統合テストであれば、データフローやワークフローが関係するケースも多々あるため、この拡張は有用です。$ pip install pytest-ordering
from src.mymodule.mymodule import MyModule
import pytest
class TestMyModule(object):
# これは2番目に実行
@pytest.mark.run(order=2)
def test_module_one(self):
mymodule = MyModule()
assert mymodule.add(1, 2) == 3
# これは最初に実行
@pytest.mark.run(order=1)
def test_module_two(self):
mymodule = MyModule()
assert mymodule.subtract(1, 2) == -1
tests/integration/mymodule/test_mymodule.py::TestMyModule::test_module_two PASSED [ 50%]
tests/integration/mymodule/test_mymodule.py::TestMyModule::test_module_one PASSED [100%]
pytest-cov
はコードカバレッジを測定するライブラリです。pip
でインストールしましょう。$ pip install pytest-cov
.
├── mymodule.py
└── tests
├── __init__.py
└── test_mymodule.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def not_tested_func():
pass
from mymodule import add, subtract
def test_add():
assert add(1, 2) == 3
def test_subtract():
assert subtract(3, 2) == 1
$ pytest --cov=mymodule tests/
collected 2 items
tests/test_mymodule.py .. [100%]
---------- coverage: platform darwin, python 3.11.2-final-0 ----------
Name Stmts Miss Cover
---------------------------------
mymodule.py 6 1 83%
---------------------------------
TOTAL 6 1 83%
mymodule.py
のテストカバレッジが83%であることを示しています。これは、モジュール内のプログラムの67%がテストによって実行されたことを意味します。$ pytest --cov=mymodule --cov-report html tests/
htmlcov/
が生成され、ディレクトリ以下にHTMLファイルが生成されます。これをブラウザで表示すると以下のようになります。$ pytest --cov=mymodule --cov-report html --cov-fail-under=80 --cov-branch tests/
-cov-fail-under=80
: カバレッジが80%未満の場合、テスト実行が失敗-cov-branch
: 条件分岐のカバレッジ計測pytest-html
です。これもpip
でインストールできます。$ pip install pytest-html
test_mymodule.py
を以下のようにします。from mymodule import add, subtract
def test_add():
assert add(1, 2) == 3
def test_subtract():
assert subtract(3, 2) == 1
def test_failure():
assert 1 == 2
test_failure()
がテスト失敗として検知されます。pytest-html
でHTML化しましょう。$ pytest --html=report.html tests/
report.html
をブラウザで表示すると以下のようになります。pytest-datadir
はテストで使用するデータファイルとディレクトリの管理を簡単にします。テスト固有のデータディレクトリへのアクセスを提供するdatadir
フィクスチャを作成します。pip
です。$ pip install pytest-datadir
.
├── mymodule.py
└── tests
├── data
│ └── test1.txt
├── test_mymodule
│ └── test2.txt
└── test_mymodule.py
tests/data/
ディレクトリには、テスト専用のデータファイルを置きます。def test_read_data_file1(shared_datadir):
# datadirはpathlib.Pathオブジェクト
data_file = shared_datadir / 'test1.txt'
with open(data_file, 'r') as f:
content = f.read()
# 実験のため単純にデータが空でないことを確認
assert content
def test_read_data_file2(datadir):
data_file = datadir / 'test2.txt'
with open(data_file, 'r') as f:
content = f.read()
assert content
test1.txt
, test2.txt
には適当に文字を書いておきます。test1
test2
pytest-datadir
は下記のような動作をしています。data
という名前のディレクトリを探査。
1, このdata
ディレクトリを指すpathlib.Path
オブジェクトをshared_datadir
の形で提供。datadir
の場合はテストファイルと同名のディレクトリを参照。pytest-mock
は、unittest.mock
ライブラリをpytest
に統合し、便利なモッカーとして機能します。pip
でインストール可能です。$ pip install pytest-mock
import requests
def get_website_title(url):
response = requests.get(url)
if response.status_code == 200:
return response.text.split('<title>')[1].split('</title>')[0]
else:
return None
pytest-mock
を使ってrequests.get
をモックします。from mymodule import get_website_title
def test_get_website_title(mocker):
# `text`と200の`status_code`を要素としてモックレスポンスを作成
mock_response = mocker.Mock()
mock_response.text = "<html><head><title>モックテストタイトル</title></head></html>"
mock_response.status_code = 200
# requests.get()をモック化し、モックレスポンスを返すように設定
mocker.patch('requests.get', return_value=mock_response)
# 実際のHTTPリクエストをお叶わずに"モックテストタイトル"を取得
result = get_website_title("http://mock.url")
assert result == "モックテストタイトル"
pytest-mock
で提供されているmocker.patch()
は一時的に本物のrequests.get
関数を定義済みのmock_response
を返すモックに置き換えrequests.get
はテスト終了時、元に戻る