FizzBuzzのテストコードを書いてみよう¶
なお、完成版は goal
の下にあります
def fizzbuzz(number: int) -> str:
match number % 3, number % 5:
case 0, 0: return "FizzBuzz"
case 0, _: return "Fizz"
case _, 0: return "Buzz"
case _, _: return str(number)
ref: Day2 Keynote:A Perfect match ―Mr. Brandt Bucher (PyCon JP 2021 カンファレンスレポート)
tests/test_core.py
を書いてみよう。まずはテストの関数を書く
def test_3の倍数のときはFizzを返す():
# 後述
3の倍数を渡して
fizzbuzz
関数を呼び出す3の倍数としてなにか1つ値を選ぶ(例
3
)
返り値は
Fizz
になるはず
呼び出すところまで書いてみる
from fizzbuzz.core import fizzbuzz
def test_3の倍数のときはFizzを返す():
_ = fizzbuzz(3)
「返り値は Fizz
になるはず」、assert
の出番!
from fizzbuzz.core import fizzbuzz
def test_3の倍数のときはFizzを返す():
assert fizzbuzz(3) == "Fizz"
テストが書けました!!
テストを実行すると、(fizzbuzz
関数は完成しているので)通ります。
fizzbuzz
関数を壊すと、テストは落ちます(壊したことに気づける!)
おすすめの実行方法
pytest -vv
--verbose
-v
でも詳しくなるが、-vv
だとassert A == B
の差分がとても分かりやすい
pytest --ff
--failed-first
: 最後に落ちたテストから実行
ケースの指定 pytest tests/test_practice.py::test_環境構築の確認
『テスト駆動Python 第2版』を一読すると、もっと知れます
注釈
日本語で書いてる!
t-wadaさんのエントリの中の 日本語テストメソッド より
日本語でコミュニケーションをとっているプロジェクトの場合、日本語でテストメソッド名を書いても良いのではないかという考え方
3A¶
def test_3の倍数のときはFizzを返す():
number = 3
actual = fizzbuzz(number)
expected = "Fizz"
assert actual == expected
- actual value:
テスト対象を実行した値
- expected value:
期待結果
3A、すなわち3つのA
https://xp123.com/articles/3a-arrange-act-assert/
Arrange 準備
Act 実行
Assert 検証
def test_3の倍数のときはFizzを返す():
# Arrange: テストの準備(データの用意など)
number = 3
# Act: テスト対象の関数を実行
actual = fizzbuzz(number)
# Assert: 実行結果が期待値と等しいかを検証
expected = "Fizz"
assert actual == expected
先のテスト(assert fizzbuzz(3) == "Fizz"
)は
Arrange と Act を書いた(
fizzbuzz(3)
)Assert を書いた
注釈
テストはAssertから書く
個人的にはどこから書いてもよいと思っている。 Assertから書くと「Actが必要だ」「Arrangeが必要だ」と詰まらずに書きやすい印象があるので、初めての方は試してみてもいいかも
なにか1つ、テストを書いてみましょう
5の倍数のとき
15の倍数のとき
3の倍数でも5の倍数でもないとき
注釈
assert
の一覧
検証したいこと |
書き方 |
|
|
|
|
|
|
assert actual is False
より assert not actual
の方がエラーメッセージが分かりやすくなります
注釈
クラスを使ってテストケースを構造化できる
TDDパートを参照
注釈
pytest-watch
テストを書いて毎回 pytest している。
ptw -- -vv --ff
pytestはテストコードを書くのをサポートする¶
テストの概念
パラメタ化テスト
モック
pytestの機能
pytestのフィクスチャ
パラメタ化テスト¶
🙅♂️「3の倍数のテストで、複数の値を実行しちゃえ」
def test_3の倍数のときはFizzを返す():
assert fizzbuzz(3) == "Fizz"
assert fizzbuzz(6) == "Fizz"
assert fizzbuzz(9) == "Fizz"
1つのテストメソッドは、1つのことを検証しよう
3のとき
6のとき
9のとき
それぞれ分けたい
注釈
アサーションルーレット(アンチパターン)
t-wadaさんのエントリの中の [ポイント] アサーションルーレット(Assertion Roulette) より
このテストが失敗したときに、どのアサーションが失敗したのかがわかりにくいのです。
では、個別にテストの関数を書くのか?
def test_3の倍数のときはFizzを返す_3の場合():
assert fizzbuzz(3) == "Fizz"
def test_3の倍数のときはFizzを返す_6の場合():
assert fizzbuzz(6) == "Fizz"
パラメタ化テスト
t-wadaさんエントリ [ポイント] パラメータ化テスト(Parameterized Test) より
ほぼ同じテスト内容でデータだけを変えたテストメソッドを(列挙であれループであれ)書いているときに、テストメソッドにパラメータを渡せればいいのに、と感じることがあると思います。
@pytest.mark.parametrize
でデコレートするimport pytest
from fizzbuzz.core import fizzbuzz
@pytest.mark.parametrize("number", [3, 6])
def test_3の倍数のときはFizzを返す(number):
assert fizzbuzz(number) == "Fizz"
注釈
parametrizeと日本語
エスケープされてしまう
pytestのtest IDにパラメータ由来の日本語を使う方法
disable_test_id_escaping_and_forfeit_all_rights_to_community_support
pytest.param
のidは未サポート)pytestのフィクスチャを使う¶
例えば、fizzbuzzの出力をテストするとしたら?
def print_fizzbuzz(upper_limit: int) -> None:
for number in range(1, upper_limit + 1):
print(fizzbuzz(number))
pytestは機能を提供している
capsys (Accessing captured output from a test function (How to capture stdout/stderr output))
capsysはpytestのビルトインのフィクスチャの1つ
フィクスチャは テストの関数の引数に書く
def test_fizzbuzzの出力のテスト(capsys):
# 後述
Actで標準出力を伴う関数を呼び出す
Assertにおいて、
capsys.readouterr()
を呼び出す標準出力は
capsys.readouterr().out
(これがactual
)標準出力が期待値と一致するかを
assert
注釈
PyCon JP 2024より「あなたのアプリケーションをレガシーコードにしないための実践Pytest入門」
(ただし、テスト駆動開発については、こちらの発表内容は誤解が見受けられるので注意(後述))
注釈
pytestのtmp_pathフィクスチャ
The tmp_path fixture (How to use temporary directories and files in tests)
注釈
capsysやtmp_pathは本当に必要ですか?(発表者の考え)
テストにもいろいろ
単体 unit
結合 integration
pytestは 幅広くテストをカバー しているように思われる(単体も結合もどちらも書ける)
print_fizzbuzz
関数のテストは書かなくてもよいと考える。print_fizzbuzz
関数は fizzbuzz
関数の出力を print
するだけだから。「テストする必要がないほど質素なコードにして、コードに恥をかかせる」(拙ブログより「 printはテストしないという考え方 」)
モック¶
モックとは、ニセモノ
たとえば、出力が変わる関数
import random
def draw_lottery() -> str:
number = random.randint(1, 6)
if number == 6:
return "超吉"
else:
return "凶"
注釈
モックの使い所
時間のかかる処理のテストを書く
外部と通信する処理のテストを書く
出力が変わると困るので、テストにおいては random.randint()
をニセモノに置き換える
pytestのmonkeypatchフィクスチャを使った例
def test_6の目が出たら超吉と返す(monkeypatch):
randint_call_count = 0
def randint_mock(a, b):
assert (a, b) == (1, 6)
nonlocal randint_call_count
randint_call_count += 1
return 6
# randintをニセモノに差し替えて、このテストでは1〜6のうち絶対6が出るものとする
monkeypatch.setattr(random, "randint", randint_mock)
assert draw_lottery() == "超吉"
assert randint_call_count == 1
絶対6を返すニセモノの関数
randint_mock
を定義したこのテストにおいては、monkeypatchを使って
random.randint
をrandint_mock
に置き換えた
How to monkeypatch/mock modules and environments
draw_lottery
のテストは、6の目が出たときの出力の検証に絞れる実装の中で
random.randint()
を呼び出しているかも合わせて検証
unittest.mock.patch
を使った例
@patch("random.randint", return_value=5)
def test_6以外の目が出たら凶と返す(mock_randint):
assert draw_lottery() == "凶"
mock_randint.assert_called_once_with(1, 6)
unittest.mock.patch
を使ってrandom.randint
をmock_randint
に置き換えたmock_randint
はunittest.mock.MagicMock
マジックメソッド を実装している
__call__()
もある
mock_randint()
と呼び出されたときの返り値を設定return_value=5
(呼び出されたら常に5)
MagicMock
は呼び出され方を記録している
自作の関数を monkeypatch.setattr()
したのと同様のことを少ないコードで実現できている
ref: patch デコレータ(unittest.mock --- 入門)
注釈
特殊メソッド __call__()
https://docs.python.org/ja/3/reference/datamodel.html#class-instances
任意のクラスのインスタンスは、クラスで
__call__()
メソッドを定義することで呼び出し可能になります。
注釈
PyCon JP 2020より「unittest.mockを使って単体テストを書こう 〜より効率的で安定したテストに〜 」
注釈
Test Doubles
http://xunitpatterns.com/Test%20Double.html
広義のモック
テストにおける代役(代役にもいろいろある)
スタブ:(HTTP通信などが)実際に動かないようにする代役
狭義のモック:呼び出す処理を実際に動かないようにする + モックを期待通り使っているかassert
拙ブログ PHPUnitのドキュメントを機にxUnit Test Patternsのサイトを確認し、Test Double・Stub・Mockを整理 〜広義のモックと狭義のモック〜
注釈
pytestのフィクスチャの使いこなし
(1)のケースのリファクタリング
@pytest.fixture
def always_6_randint(monkeypatch):
randint_call_count = 0
def randint_mock(a, b):
assert (a, b) == (1, 6)
nonlocal randint_call_count
randint_call_count += 1
return 6
monkeypatch.setattr(random, "randint", randint_mock)
yield
assert randint_call_count == 1
def test_6の目が出たら超吉と返す_リファクタ版(always_6_randint):
assert draw_lottery() == "超吉"
monkeypatchを使った自作のフィクスチャを定義
「6の目が出たら超吉と返す」は自作のフィクスチャを指定して実装
「6の目が出たら超吉と返す」ではmonkeypatchを指定していないが、自作のフィクスチャが指定しているので、これでmonkeypatchを含めて動く
自分で書いたフィクスチャによって、Arrangeをスッキリさせられる
pytest --setup-show でフィクスチャのsetup順が確認できる
小まとめ¶
「(本ワークショップで)体験する概念」の1と2をカバーしました
テスティングフレームワークpytestを使って、テストコードを書いた
ここで書いたのは開発者のためのテスト
2点目の参考:拙ブログ 誰のためのテスト?