python 3.11.2
OS macOS Ventura 13.4.1
以前、【初級エンジニア向け】テストって何?から一歩先へいくテスト習慣付けのススメというコンテンツを執筆したのですが、実際にユニットテスト(単体テスト)を書く作業についてきちんと紹介します。
今回はPythonとそのライブラリであるpytestを使ったユニットテストについて順を追って説明していきます。
Pythonにはもともと備わっている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
テストも実行可能実際の使い方を見ていくために、pytestのインストールをしていきます。
今回はpyenvを使って余計なライブラリが干渉しない状態で進めていきます。
当たり前ですが、インストールしただけで何もテストファイルがない場合はそのままテストが通ります。
最小でテストを実行してみましょう。
test_xxx.py
やxxx_test.py
のようにファイル名にtest
をつけるとテストファイルとして認識されます。
これを実行すると以下のようにテストが成功します。
失敗するパターンもやってみましょう。
このように失敗した箇所がわかりやすく表示されます。
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など)で出力 |
自然語のコメントをつけることで「何を期待していたが得られなかったか」をよりわかりやすくできます。
基本的にpytest
は予約語であるassert
を覚えておけば基本的な使い方は可能です。
assert
に続くコードがTrue
を返せば成功、Flase
を返せばテストは失敗します。
それ以外にも、pytest
独自のデコレータや関数を覚えておけば便利に使えます。
例外を検出する際に使います。
例として、2つの数値を除算する関数があるとします。ゼロで割ったときにZeroDivisionError
が発生するようにします。
そして、ゼロで割ったときのテストを用意します。
with
でそのコンテキスト内に発生した例外をキャッチpytest.raises(ZeroDivisionError)
は、with
ブロック内でZeroDivisionError
が発生することを保証with
ブロック内のコードがZeroDivisionError
を発生させなければテスト失敗または、match引数を使えば例外メッセージが特定の文字列にマッチするかどうかをテストできます。
このデコレータはフィクスチャを宣言します。
フィクスチャとは、テストの中で使いたい特定のオブジェクトや値を返す関数のことです。たとえば、データベース接続や設定データなどを返すようなものです。
特定のタグでテスト関数をマークします。
これにより、テストを選択的に実行できます。カスタムマーカーを定義できますし、組み込みのものもいくつかあります。
このデコレータは、特定の条件下あるいは無条件でテスト関数をスキップするようにマークします。
このデコレータは、条件がTrue
の場合にテストをスキップするようにマークします。
このデコレーターを使うと、テスト関数を異なる引数で複数回実行できます。
このデコレータは、テストが失敗することを想定していることを表します。
pytest
のディレクトリ構成はプロジェクトごとに合意が取れたものであればチーム内で共有しやすいですが、一例を提示しておきます。
このケースだと基本的にsrc/
ディレクトリへ実際のアプリケーションコードを入れ、tests/
にすべてのテストファイルを集約しています。
先述したオプション-k
による実行するテストの指定以外にも、以下の方法で実行するテストを指定可能です。
例として、先ほどのディレクトリ構成のmymodule
にクラスを作成してみます。
これは以下それぞれの指定の仕方でテスト可能です。
pytest tests/unit/mymodule/test_mymodule.py
pytest tests/unit/mymodule/test_mymodule.py::test_module
テストをまとめるためにクラス化した場合は以下のようにできます。
pytest tests/unit/mymodule/test_mymodule.py::TestMyModule
pytest tests/unit/mymodule/test_mymodule.py::TestMyModule::test_module
pytest
では、フィクスチャはセットアップアクションとティアダウン(またはポスト)アクションの両方を持つことができます。これらはそれぞれ、フィクスチャを使用するテスト関数の前と後に実行されるアクションです。
セットアップコードはフィクスチャのyield
文の前に行うすべてのことで、ティアダウン(またはポストアクション)コードはyield
文の後に行うすべてのことです。
たとえば、データベースシステムをテストしているとします。テストを実行する前にデータベースへの接続を確立し、 テストが終わったら接続を閉じたいとします。
pytest
では、conftest.py
はディレクトリ内の複数のテストファイルで利用可能なフック、フィクスチャ、その他の設定を定義できます。主なメリットはコードの重複を避け、設定とフィクスチャのための一元的な設定データとして機能することです。
conftest.py
の使い方は以下の通りです。
conftest.py
をテストファイルがあるディレクトリ、または任意の親ディレクトリに設置(pytestは自動でconftest.py
を検知する)conftest.py
でフィクスチャを定義conftest.py
でフックを使う例として、データベースフィクスチャを作ってみましょう。
カスタムオプションを定義した場合、以下のような形でpytest
を実行できます。
pytest
では、テストの一般的な操作を支援するヘルパー関数を作成し、コードの再利用性と可読性を高めることができます。デフォルトでは組み込みのヘルパーを持ちませんが、ユーティリティ関数を作成するための標準的なPythonのアプローチを利用するだけで簡単に作成できます。
例として、Webアプリケーションをテストしていて、ランダムなユーザーデータを生成する必要があるとします。以下のようにヘルパーを作成していきます。
これを以下のようにテストで利用します。
ヘルパーを利用する際は、以下のことに注意してください。
utils.py
のように別のファイルとして設置してインポートして使うべしpytest
はその他の外部ライブラリを利用することでより便利に利用できます。
pytest-ordering
を使うと、テストの順番を制御可能です。統合テストであれば、データフローやワークフローが関係するケースも多々あるため、この拡張は有用です。
インストールにはpipからできます。
pytest-cov
はコードカバレッジを測定するライブラリです。
pip
でインストールしましょう。
以下のようなディレクトリ構成でテストをするとします。
ここで以下のコマンドを実行します。
すると、以下のような結果を得られるはずです。
この結果は、mymodule.py
のテストカバレッジが83%であることを示しています。これは、モジュール内のプログラムの67%がテストによって実行されたことを意味します。
HTMLレポートのようなより詳細なレポートが欲しい場合は次のようにします。
実行すると、htmlcov/
が生成され、ディレクトリ以下にHTMLファイルが生成されます。これをブラウザで表示すると以下のようになります。
pytest-cov結果
カバレッジ結果に含めるファイルや除外するファイル、ディレクトリを指定できます。
-cov-fail-under=80
: カバレッジが80%未満の場合、テスト実行が失敗-cov-branch
: 条件分岐のカバレッジ計測続いてpytest-html
です。これもpip
でインストールできます。
先ほどのtest_mymodule.py
を以下のようにします。
この状態だとtest_failure()
がテスト失敗として検知されます。
pytest-html
でHTML化しましょう。
これによって出力されるreport.html
をブラウザで表示すると以下のようになります。
pytest-html結果
上記のレポートは下記のような結果を示しています。
pytest-datadir
はテストで使用するデータファイルとディレクトリの管理を簡単にします。テスト固有のデータディレクトリへのアクセスを提供するdatadir
フィクスチャを作成します。
インストールはpip
です。
今回は以下のようなディレクトリ構成で試してみます。
tests/data/
ディレクトリには、テスト専用のデータファイルを置きます。
test1.txt
, test2.txt
には適当に文字を書いておきます。
もしこれが空の場合はテスト失敗、上記のように文字が入っていればテスト合格となります。
pytest-datadir
は下記のような動作をしています。
data
という名前のディレクトリを探査。
1, このdata
ディレクトリを指すpathlib.Path
オブジェクトをshared_datadir
の形で提供。datadir
の場合はテストファイルと同名のディレクトリを参照。pytest-mock
は、unittest.mock
ライブラリをpytest
に統合し、便利なモッカーとして機能します。
これもまた、pip
でインストール可能です。
例として、特定のWebサイトからタイトルを抽出したいとします。
この関数は与えられたURLからWebサイトのタイトルを取得します。実際のHTTPリクエストを行わずにこの関数をテストするには、pytest-mock
を使ってrequests.get
をモックします。
pytest-mock
で提供されているmocker.patch()
は一時的に本物のrequests.get
関数を定義済みのmock_response
を返すモックに置き換えrequests.get
はテスト終了時、元に戻る規模が大きいアプリケーションだけでなく、使い捨てのようなコードも含め、テストを書く習慣をつけることでコードの品質は格段に上がります。自分がコードに何を期待し、何を成し遂げなくてはいけないかを自問自答する意味でもテストを書く習慣をつけていきましょう。
「この人すごいな」と感じるエンジニアとチームになるとわかりますが、必ずといっていいほどテストをプロジェクト初期から書いています。
PullRequestを送っても「テストを書いてください」と指示されるため、Approveされずにpushし直す作業が発生するため、そのような無駄を省くためにも普段からテストを習慣づけましょう。
もしまだ現在のプロジェクトにテストコードが存在しなければ、ぜひあなたが牽引してテストをチーム内で流行らせていきましょう。