pytestのインストールと初めてのテスト
手動チェックが破綻する理由、素のassertではほとんど何も分からない理由、そしてpytestをインストールして最初のテストファイルを記述・実行する方法を学びます。ドット表示、サマリー行、-q、-v、終了コードについても解説します。
どうやって学びますか?
解決する問題
以下は小さな関数と、私たちがこのような関数をチェックするときによくやる方法です — 実行して出力結果を見てみます:
def discounted(price, percent):
return price - price * percent / 100
print(discounted(200, 10))
print(discounted(80, 25))
print(discounted(99, 0))180.0
60.0
99.03つの数値を読み、頭の中で計算し、正しいと判断します。これも立派なテストです。ただし手動のテストであり、2つの問題があります。
1つ目は、規模の拡大に対応できない(スケールしない)ことです。3つの数値なら目で見て確認するのも簡単です。しかし、それぞれ5つのケースを持つ30個の関数があり、変更のたびにそれらすべてを再確認するとなるとそうはいきません — そして、そんなことは誰もやりません。
2つ目はさらに深刻です。1か月後、誰かがこの関数を整理(リファクタリング)したとしましょう:
def discounted(price, percent):
return price * (1 - percent // 100)$ python -c "from prices import discounted; print(discounted(200, 10))"
200一見もっともらしく見え、エラーもなく実行されます。しかし//は整数除算なので、10 // 100は0になり、すべての割引が気づかないうちに消え去ってしまいました。以前は動いていたコードが動かなくなることをリグレッションと呼びます。そしてリグレッションこそ、手動チェックが見逃してしまうものそのものです — なぜなら誰も古い数値をあらためて確認し直さないからです。
解決策は、期待される答えをコードとして書き出しておき、マシンが毎回チェックできるようにすることです。Pythonにはそのための文であるassertがすでに備わっています。prices.pyの隣にcheck_prices.pyというファイルを作成し、チェックを記述します:
from prices import discounted
assert discounted(200, 10) == 180.0
assert discounted(80, 25) == 60.0
assert discounted(99, 0) == 99
print("all good")元の関数で実行した場合:
$ python check_prices.py
all good「整理された」関数で実行した場合:
$ python check_prices.py
Traceback (most recent call last):
File "/home/you/shop/check_prices.py", line 3, in <module>
assert discounted(200, 10) == 180.0
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionErrorリグレッションを検知できました。これは前進です。しかし、何が表示されたかを見てください。AssertionErrorだけで、それ以外には何もありません。discounted(200, 10)が実際に何を返したのかも分かりません。さらに、スクリプトは最初の失敗で停止してしまったため、残りの2つのチェックがパスするかどうかも分かりません。
この章では、この両方を解決するツールであるpytestをインストールします。
この章を終える頃には
- 自動テストが手動チェックの見逃すリグレッションを検知できる理由を説明できるようになる
uvまたはpipでpytestをインストールし、pytest --versionで確認できるようになる- pytestが自動で見つけ出せるテストファイルを作成し、実行できるようになる
- ドット表示とサマリー行を読み解き、
-qと-vを切り替えられるようになる - 終了コード
0と1の意味と、それがなぜ重要なのかを説明できるようになる
前提条件: テストを書く前に。また、Python 3と関数の書き方の知識が必要です。
テストを書く前に
前章では何をテストするかを決めました。この章では、テストコードの最初の1行を実行できるようになる前に準備しておくべき具体的な事柄を扱います。準備すべきことは4つあり、どれか1つでも欠けていると分かりにくい独自のエラーが発生します — そのため、順番に確認しておく価値があります。
1. 1つのプロジェクトフォルダ、1つの環境。 この章のすべての作業は単一のフォルダ内で行われます:
shop/
├── .venv/ the project's own Python environment
├── pyproject.toml created by uv; lists pytest as a dev dependency
├── prices.py the code under test
└── test_prices.py the testsなぜ専用の環境が必要なのでしょうか?それは、pytestがコードをインポートするのと同じPython環境にインストールされている必要があるからです。pytestがあるPythonに入っていて、プロジェクトのパッケージが別のPythonに入っていると、実際の問題は「ツールがないこと」なのに「コードが見つからない」ように見えるエラーが発生します。プロジェクトごとに.venvを用意することで、そうした迷いをなくせます。
2. pytestがインストールされていること(その環境内に) — これについては次のセクションで行います。
3. コードがインポート可能であること。 テストファイルは通常のPythonコードです。その1行目はfrom prices import discountedになります。そのため、テストを書く前に、プロジェクトフォルダからこのインポートが正しく機能するか確認してください:
$ python -c "from prices import discounted; print(discounted(200, 10))"
180.0この行が失敗する場合、pytestもまったく同じように失敗します — より長いレポートに包まれて表示されるだけです。これを単体で確認するのにかかる時間は5秒ほどですが、「コードが見つからない」のか「コードが間違っている」のかを切り分けられます。また、モジュール名が有効なPython名である必要があることも意味します。prices.pyはインポートできますが、my-prices.pyや2prices.pyはインポートできません。
4. 約束(契約)をケースとして書き出すこと。 テストを入力する前に、discountedが何を保証しているのかを、1ケース1行で書き出します:
| ケース | 入力 | 期待値 | | --- | --- | --- | | 典型的な割引 | discounted(200, 10) | 180.0 | | 2つ目の異なる割引 | discounted(80, 25) | 60.0 | | 境界値:割引なし | discounted(99, 0) | 99 |
そして同様に意図的に、リストに含めないものも明確にします。マイナスのパーセンテージや100を超える数値:この関数はそれらについてまだ何も保証しておらず、テストを書いても現在たまたま行われている動作を固定してしまうだけです(不正な入力をエラーとして拒否することは、それ自体が1つの保証です — 第4章で扱います)。また、Python自体の算術演算も含めません。テストするのは*が掛け算を行うことではなく、自分自身が書いた計算式だからです。
なぜ通常のケースが1つではなく2つあるのでしょうか?章の最初に出てきた「整理された」関数と照らし合わせて3行目を見てみてください。99 * (1 - 0 // 100)は依然として99のままです。0%のケースは、バグがあってもパスしてしまいます。境界ケースだけで作られた計画はまさに重要なバグを見逃す可能性があり、典型ケース1つだけで作られた計画は境界を見逃します。各行は、他の行では検知できない何かを捉えるために存在しているのです。
この章の残りの部分では、この表をテストファイルに変換していきます。
pytestのインストール
pytestはPython本体の一部ではありません。プロジェクトにインストールして使うパッケージです。コードを開発するためのツールであり、プログラムが実行時に必要とするものではないため、開発用依存関係として追加します。
プロジェクトでuvを使用している場合は、プロジェクトフォルダで以下を実行します:
$ uv add --dev pytest
Resolved 7 packages in 59ms
Installed 5 packages in 18ms
+ iniconfig==2.3.1
+ packaging==26.3
+ pluggy==1.6.0
+ pygments==2.21.0
+ pytest==9.1.1uvはこれをpyproject.toml内のdevグループに記録するため、後からプロジェクトをセットアップする誰もが同じツールを利用できます:
[dependency-groups]
dev = [
"pytest>=9.1.1",
]仮想環境内で通常のpipを使用している場合は、仮想環境を有効化して以下を実行します:
$ python -m pip install pytestpipによってダウンロードの進捗が表示され、最後は次のように終わります:
Successfully installed iniconfig-2.3.1 packaging-26.3 pluggy-1.6.0 pygments-2.21.0 pytest-9.1.1単なるpipではなくpython -m pipを使用することで、これから実行するPythonそのものにパッケージが確実にインストールされます — 「インストールしたのに見つからない」というトラブルのよくある原因を防ぐことができます。
それでは、正しくインストールできたか確認してみましょう:
$ pytest --version
pytest 9.1.1先へ進む前に1つ注意点があります。uvでは、コマンドの前にuv runを付けることでプロジェクトの環境内で実行されます — つまりuv run pytest --versionです。有効化された仮想環境内であれば、単にpytestと入力するだけで十分です。このコースの以降の説明では通常のpytestと表記しますので、uvを使用している場合は必要に応じて先頭にuv runを補ってください。
初めてのテストファイル
pytestが必要とする命名規則は2つだけです:
- ファイル名が
test_で始まる(または_test.pyで終わる)こと - 実行したい各関数の名前が
testで始まること
元の正しいprices.pyに戻し、その隣にtest_prices.pyを作成します:
from prices import discounted
def test_ten_percent_off():
assert discounted(200, 10) == 180.0
def test_quarter_off():
assert discounted(80, 25) == 60.0
def test_no_discount():
assert discounted(99, 0) == 99check_prices.pyにあった各チェックが、それぞれ独立した小さな関数になりました。そしてそれぞれの名前が、何をチェックしているのかを表しています。printも、登録すべきテストのリストも、if __name__ == "__main__"もありません。それでは、プロジェクトフォルダからpytestを実行してみましょう:
$ pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
collected 3 items
test_prices.py ... [100%]
============================== 3 passed in 0.01s ===============================pytestはフォルダ内を探索し、ファイル名を手がかりにtest_prices.pyを見つけ、関数名を手がかりに3つの関数を見つけ、それぞれを実行して結果を報告しました。
出力を読み解く
1行ずつ見ていきましょう。
platform ...— 実行されたPythonとpytestのバージョン。2台のマシン間で結果が異なるときに確認すると役立ちます。rootdir: /home/you/shop— pytestがプロジェクトのルートとして扱うフォルダ。collected 3 items— 見つかったテストの数。テストを見つける処理をコレクションと呼びます。この数値が予想より少ない場合は、どこかで名前を間違えています。test_prices.py ...— 実行順にテストごとに1文字が表示されます。.はパスしたことを意味します。[100%]— 実行全体の進捗状況。3 passed in 0.01s— サマリー行。最初に確認すべき行です。
ここで、「整理された」discounted関数に戻してもう一度実行してみましょう。今回は、この後すぐ詳しく説明する-qを付けて実行します:
$ pytest -q
FF. [100%]
=================================== FAILURES ===================================
_____________________________ test_ten_percent_off _____________________________
def test_ten_percent_off():
> assert discounted(200, 10) == 180.0
E assert 200 == 180.0
E + where 200 = discounted(200, 10)
test_prices.py:5: AssertionError
_______________________________ test_quarter_off _______________________________
def test_quarter_off():
> assert discounted(80, 25) == 60.0
E assert 80 == 60.0
E + where 80 = discounted(80, 25)
test_prices.py:9: AssertionError
=========================== short test summary info ============================
FAILED test_prices.py::test_ten_percent_off - assert 200 == 180.0
FAILED test_prices.py::test_quarter_off - assert 80 == 60.0
2 failed, 1 passed in 0.01sプログレス行はFF.になりました。失敗ごとにFが表示されます。先ほどの素のAssertionErrorと比べてみてください:
- すべてのテストが実行された — 1つのテストが失敗しても他のテストは停止しなかったため、0%のケースが今でも動作していることが分かります
- 各失敗でテスト名と行番号が表示される
assert 200 == 180.0によって、関数が実際に返した値が示される
このレポートの詳細な読み方については、次章で丸ごと扱います。現時点では、サマリー行が2 failed, 1 passedに変化したこと、そして以前から知っていたあのassertが自分自身の理由を説明してくれるようになったことだけを確認しておいてください。
-qと-v:少なく、多く
デフォルトの出力はその中間に位置します。2つのフラグを使うことで、どちらの方向にも調整できます。
-q(quiet)はヘッダーを省略します:
$ pytest -q
... [100%]
3 passed in 0.01s-v(verbose)は各テストを専用の1行に表示し、完全な名前を示します:
$ pytest -v
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
cachedir: .pytest_cache
rootdir: /home/you/shop
collecting ... collected 3 items
test_prices.py::test_ten_percent_off PASSED [ 33%]
test_prices.py::test_quarter_off PASSED [ 66%]
test_prices.py::test_no_discount PASSED [100%]
============================== 3 passed in 0.01s ===============================test_prices.py::test_ten_percent_offはそのテストのノードIDです:ファイル名、2つのコロン、関数名です。これは後で単一のテストを実行するときにも使用します。分かりやすいテスト名を付ける価値が発揮されるのもここです — -vの一覧において、test_quarter_offは何がパスしたのかを伝えてくれますが、test_2では何も分かりません。
頻繁にテストを実行していて判定結果だけを知りたいときは-qを使い、どのテストが実行されたのかを正確に確認したいときは-vを使います。
.pytest_cacheとは何ですか? 前回の実行でどのテストが失敗したかなど、実行間で情報を記憶するためにpytestが作成するフォルダです。削除しても問題なく、バージョン管理に含めるべきではありません。終了コード:マシンが結果を読み取る方法
人間はサマリー行を読みます。しかしスクリプトやエディタ、ビルドサーバーはサマリー行を読めません — 代わりに終了コードを読み取ります。これは、すべてのプログラムが終了時にシェルに返す数値です。Unixシェルでは、直前の終了コードが$?に格納されます:
$ pytest -q
... [100%]
3 passed in 0.01s
$ echo $?
0そして壊れた関数で実行し、2 failed, 1 passedで終わった後の場合:
$ echo $?
10— すべてのテストが収集され、すべてパスした1— テストが実行され、少なくとも1つが失敗した
(PowerShellでは、この変数は$LASTEXITCODEです。)これこそが、コードがマージされる前にテストを自動実行できる仕組みです:ゼロ以外の終了コードは処理を停止させます。終了コードには他にもいくつか種類があり、そのうちの1つはこの後の「問題が起きたとき」で登場します。
テストファイルの配置場所
この章では、テストファイルはコードのすぐ隣に配置されています:
shop/
├── prices.py
└── test_prices.py数個のファイルだけであれば、これでも問題ありません。しかし、大半のプロジェクトではテストを専用のtests/フォルダにまとめて配置します。どちらの方法でも、pytestはファイル名をもとに見つけ出してくれます。実際のプロジェクトをどのように構成すべきか、そしてその際に何が変わるのかについては第3章で扱います。
完全な例
点数をアルファベットの成績に変換する関数、grades.py:
def letter_grade(score):
"""Turn a score from 0 to 100 into a letter."""
if score >= 80:
return "A"
if score >= 60:
return "B"
if score >= 40:
return "C"
return "F"そしてそのテスト、test_grades.py:
from grades import letter_grade
def test_top_score_is_an_a():
assert letter_grade(95) == "A"
def test_exactly_eighty_is_still_an_a():
assert letter_grade(80) == "A"
def test_just_below_eighty_is_a_b():
assert letter_grade(79) == "B"
def test_middle_score_is_a_c():
assert letter_grade(50) == "C"
def test_zero_is_an_f():
assert letter_grade(0) == "F"$ pytest -v
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
cachedir: .pytest_cache
rootdir: /home/you/grades
collecting ... collected 5 items
test_grades.py::test_top_score_is_an_a PASSED [ 20%]
test_grades.py::test_exactly_eighty_is_still_an_a PASSED [ 40%]
test_grades.py::test_just_below_eighty_is_a_b PASSED [ 60%]
test_grades.py::test_middle_score_is_a_c PASSED [ 80%]
test_grades.py::test_zero_is_an_f PASSED [100%]
============================== 5 passed in 0.01s ===============================注目すべき点が2つあります。
1つ目は、80と79でのテストです。これらはミスが発生しやすい境界値の両側に位置しています。ついうっかりやりがちなミスとして、score >= 80をscore > 80に変更し、簡潔な形式で実行してみます:
$ pytest -q
.F... [100%]
=================================== FAILURES ===================================
______________________ test_exactly_eighty_is_still_an_a _______________________
def test_exactly_eighty_is_still_an_a():
> assert letter_grade(80) == "A"
E AssertionError: assert 'B' == 'A'
E
E - A
E + B
test_grades.py:9: AssertionError
=========================== short test summary info ============================
FAILED test_grades.py::test_exactly_eighty_is_still_an_a - AssertionError: as...
1 failed, 4 passed in 0.01s2つ目のテストであるtest_exactly_eighty_is_still_an_aが失敗したテストです。80の結果が'B'になってしまいました。95だけのテストでは、このミスに決して気づくことはできなかったでしょう。
2つ目は名前です。-vの一覧において、各行は関数の振る舞いについての1文のように読めます。数か月後にテストが失敗したときでも、名前を見るだけでどの約束が破られたのかが分かります。
問題が起きたとき
zsh: command not found: pytest(またはbash: pytest: command not found) シェルがpytestプログラムを見つけられません。インストールされていないか、有効化されていない仮想環境にインストールされているかのどちらかです。uvの場合はuv run pytestを実行します。pipの場合は、まず仮想環境を有効化してください。python -m pytestも使用でき、pythonコマンドが指しているPythonを使ってpytestを実行します。
/usr/bin/python3: No module named pytest python -m pytestを実行しましたが、そのPythonにpytestがインストールされていません。先頭のパスはそのPythonがどれであるかを示しています — ここではプロジェクトのものではなく、システムのPythonです。使用する想定の環境にpytestをインストールしてください。
ModuleNotFoundError: No module named 'price'(コレクション時) テストファイルがコードをインポートできませんでした — ここではfrom prices import ...ではなくfrom price import ...とタイプミスしたことが原因です:
$ pytest -q
==================================== ERRORS ====================================
_______________________ ERROR collecting test_prices.py ________________________
ImportError while importing test module '/home/you/shop/test_prices.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/usr/lib/python3.12/importlib/__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
test_prices.py:1: in <module>
from price import discounted
E ModuleNotFoundError: No module named 'price'
=========================== short test summary info ============================
ERROR test_prices.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01sFAILEDではなくERRORという単語が表示されていることに注目してください。ファイルを読み込むことすらできなかったため、テストは1つも実行されておらず、終了コードは2になります。最後のE行を読み、それから「テストを書く前に」で説明したインポートのチェック — python -c "from prices import discounted" — が単体で成功するまで確認してください。
no tests ran in 0.01s pytestは実行されましたが、何も収集できませんでした。ファイル名がtest_で始まっていない(tests_prices.py、prices_tests.py、test.pyはいずれも見逃されます)か、あるいは関数名がtestで始まっていません(check_quarter_offは何の警告もなく無視されます)。ここでの終了コードは0ではなく5になります。そのため、自動実行において「何も見つからなかった」ことを「すべてパスした」と誤認することはありません。
明らかに失敗するはずのテストがパスする assertが抜けていないか確認してください:
from prices import discounted
def test_quarter_off():
discounted(80, 25) == 999$ pytest -q
. [100%]
1 passed in 0.01s比較が計算された後、そのまま捨てられてしまっています。テストが失敗するのは内部で例外が送出されたときだけであり、assertがなければ何も送出されません。
PytestReturnNotNoneWarning: Test functions should return None assert ...の代わりにreturn discounted(99, 0) == 99と書いてしまいました。pytestはテストの戻り値を無視するため、比較結果が真でも偽でもこのテストはパスしてしまいます。警告メッセージでもDid you mean to use assert instead of return?と指摘されています。
ステップ 4 / 6 — 予想する
理解度をチェック
このファイルがtest_double.pyとして保存され、pytest -qが実行されます。出力の1行目はどの文字で始まりますか?
def double(n):
return n * 2
def test_two():
assert double(2) == 4
def test_zero():
assert double(0) == 1
def test_negative():
assert double(-3) == -6- AF
- B..F
- C.F.
- DF..
999は明らかに間違った答えですが、pytestは1 passedと報告します。なぜですか?
def discounted(price, percent):
return price - price * percent / 100
def test_quarter_off():
discounted(80, 25) == 999- Apytestは関数が実行されたことのみをチェックし、その結果はチェックしないから
- Bその行に`assert`がないため — 比較が計算された後にそのまま捨てられているから
- C小数は`==`で比較できないから
- D関数名が`test_`で始まっていないから
正しいテストが含まれているファイルがdouble_checks.pyという名前になっています。そのフォルダでpytestを実行すると何が起きますか?
- Apytestが`ImportError`で停止する
- B`no tests ran` — ファイル名が`test_`で始まっていないため探索されず、終了コードは`5`になる
- C関数名が`test_`で始まっているため、すべてのテストが実行される
- Dpytestがファイルを自動的に`test_double_checks.py`にリネームしてくれる
回答にはアカウントが必要です
ログインして答えを確認
問題は上にあります。頭の中で答えを考えることがいちばん大切です。ログインすると、正解・解説・3 段階のヒントを確認できます。
やってみよう
フォルダを作成し、その中に2つの関数を持つtemperature.pyを作成します:
c_to_f(celsius)—celsius * 9 / 5 + 32を返すf_to_c(fahrenheit)—(fahrenheit - 32) * 5 / 9を返す
次に、そのフォルダの環境にpytestをインストールし、pytest --versionで確認します。
少なくとも5つのテストを含むtest_temperature.pyを作成してください。水の氷点(0と32)、水の沸点(100と212)、そして両方の温度目盛りで同じ値になる-40を含めます。すべてのテストに、何をチェックしているのかを表す名前を付けてください。
これを3つの方法(pytest、pytest -q、pytest -v)で実行し、いずれかの実行の後に終了コードを確認します。
次に、意図的にバグを埋め込んでみます:c_to_fの9 / 5を5 / 9に変更します。実行する前に、どのテストが失敗し、プログレス行がどのようになるかを予測してみてください。pytest -qを実行して結果を比較し、終了コードを再度確認します。
最後に、test_temperature.pyをtemperature_check.pyにリネームしてpytestを実行します。何と表示されるか、そしてその終了コードを確認してください。確認したら元の名前に戻します。「何も見つからなかった」ときにどうなるかを知っておけば、後で何が起きたか分からず無駄な時間を過ごさずに済みます。
解答例
コードを書く前に、まずは計画からです — 先ほどと同じ表を作成します:
| ケース | 入力 | 期待値 | | --- | --- | --- | | 水が凍る | c_to_f(0), f_to_c(32) | 32, 0 | | 水が沸騰する | c_to_f(100), f_to_c(212) | 212, 100 | | 2つの目盛りが一致する | c_to_f(-40), f_to_c(-40) | -40, -40 |
temperature.py:
def c_to_f(celsius):
return celsius * 9 / 5 + 32
def f_to_c(fahrenheit):
return (fahrenheit - 32) * 5 / 9テストを書く前に、プロジェクトフォルダからインポートのチェックを行います:
$ python -c "import temperature; print('importable')"
importabletest_temperature.py:
from temperature import c_to_f, f_to_c
def test_water_freezes_at_32_f():
assert c_to_f(0) == 32
def test_water_boils_at_212_f():
assert c_to_f(100) == 212
def test_32_f_is_zero_c():
assert f_to_c(32) == 0
def test_212_f_is_100_c():
assert f_to_c(212) == 100
def test_minus_40_is_the_same_in_both_scales():
assert c_to_f(-40) == -40
assert f_to_c(-40) == -40$ pytest -q
..... [100%]
5 passed in 0.01s
$ echo $?
0
$ pytest -v
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
cachedir: .pytest_cache
rootdir: /home/you/temperature
collecting ... collected 5 items
test_temperature.py::test_water_freezes_at_32_f PASSED [ 20%]
test_temperature.py::test_water_boils_at_212_f PASSED [ 40%]
test_temperature.py::test_32_f_is_zero_c PASSED [ 60%]
test_temperature.py::test_212_f_is_100_c PASSED [ 80%]
test_temperature.py::test_minus_40_is_the_same_in_both_scales PASSED [100%]
============================== 5 passed in 0.01s ===============================ここで意図的なバグとして、c_to_fの9 / 5を5 / 9に変更します:
$ pytest -q
.F..F [100%]
=================================== FAILURES ===================================
__________________________ test_water_boils_at_212_f ___________________________
def test_water_boils_at_212_f():
> assert c_to_f(100) == 212
E assert 87.55555555555556 == 212
E + where 87.55555555555556 = c_to_f(100)
test_temperature.py:9: AssertionError
___________________ test_minus_40_is_the_same_in_both_scales ___________________
def test_minus_40_is_the_same_in_both_scales():
> assert c_to_f(-40) == -40
E assert 9.777777777777779 == -40
E + where 9.777777777777779 = c_to_f(-40)
test_temperature.py:21: AssertionError
=========================== short test summary info ============================
FAILED test_temperature.py::test_water_boils_at_212_f - assert 87.55555555555...
FAILED test_temperature.py::test_minus_40_is_the_same_in_both_scales - assert...
2 failed, 3 passed in 0.01s
$ echo $?
1そしてリネームしたファイルの場合:
$ pytest -q
no tests ran in 0.01s
$ echo $?
5なぜこのように書かれているのか:
- 1つのテストにつき1つの振る舞い、1文としての名前。
-vの一覧において、各行は何が約束されているかを示しています。test_water_boils_at_212_fが失敗したとき、それ以上読み進めるまでもなく何が壊れたのかが分かります。 - 既知の確認可能な値。
0/32、100/212、-40は計算機を使わなくても確認できる事実であるため、失敗した場合は怪しい期待値ではなくコード側に原因があることが分かります。 - 双方向のテスト。
c_to_fとf_to_cは別々のコードなので、それぞれ独自のテストが必要です。片方が正しいからといって、もう片方についても正しいとは言えません。 - バグの実行結果こそが最も雄弁な部分。 失敗したのは2つのテストだけであり、
test_water_freezes_at_32_fはその中に含まれていませんでした。0 * 5 / 9 + 32は依然として32のままです。ゼロを掛けるとどんな乗数も隠されてしまうからです。計画に複数のケースを用意した理由はまさにここにあります — 氷点だけのテストスイートでは、バグが存在していてもパスしてしまっていたはずです。-40のテストには2行のassertがあります。最初の行が失敗したため、2行目は実行されませんでした。両者は同じ約束を記述しているため、ここではこれで問題ありません。 - 終了コード —
0、1、5— は、最もよく目にする3つです:すべてパス、何かが失敗、何も見つからない。