好きとか嫌いとかはいい、練習してテストを書けるようになるんだ

好きとか嫌いとかはいい、 練習してテストを書けるよ うになるんだ

日本語資料・ English talk です

Event:

PyCon APAC 2023

Presented:

2023/10/27 nikkie

お前、誰よ

  • nikkie(にっきー) / @ftnext @ftnext

  • 株式会社ユーザベースのデータサイエンティスト(We're hiring!

https://drive.google.com/uc?id=19PMMnkqDiFMCJBPwoA1B51ltQBG0y4kL

お前、誰よ 続)Pythonとアニメが好き

いまはまだできなくても大丈夫。
これからできるようになればいい

最近ハマってる #ミリアニネタバレ感想

練習してテストを書けるようになるんだ

  • 前提:Pythonの 関数 が書ける

  • いまはまだテストコードを書いたことがなくて大丈夫

お品書き(兼 持ち帰れるもの)

  1. テストコードが書けるメリット

  2. doctestの使い方

  3. pytestの使い方

動作環境 & サンプルコード

テストコードが書けるメリット

一度書いたコードは改善(変更)を免れない

  • 持てる知識を全て動員して書いたが、より適切な文法を知らなかった

  • 新しく知った書き方 で書き直したい ➡️ 学びになり、Pythonの力がつく💪

例:FizzBuzz

def fizzbuzz(number: int) -> str:
    if number % 3 == 0 and number % 5 == 0:
        return "FizzBuzz"
    elif number % 3 == 0:
        return "Fizz"
    elif number % 5 == 0:
        return "Buzz"
    else:
        return str(number)

https://pycamp.pycon.jp/textbook/2_intro.html#fizzbuzz リスト2.14

Structural Pattern Matching (Python 3.10〜)

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)

https://gihyo.jp/news/report/01/pyconjp2021/0002

裏で「Introduction to Structural Pattern Matching

書き換えで振る舞いを変えていないだろうか?

不安 に対処するいくつかのアプローチ

(A) 祈る 🙏

  • 🙏🙏「どうか変わっていませんように」🙏🙏

  • 振る舞いを変えていないか不安だが、 何も確認はしない

(B) 手で動作確認 ✋

  • 例えば対話的に fizzbuzz 関数を実行

>>> from fizzbuzz import fizzbuzz  
>>> fizzbuzz(15)  
'FizzBuzz'
  • 安心できるが、 関数の数が増えて いくと現実的ではなさそう

(C) コードを書いて動作確認 🤖

  • この発表の本題

  • 「手で動作確認」の 自動化 (テストコードを書く)

  • プログラムで使う部品のコードは、プログラムを書いて動作確認するという考え方

用語紹介(1/2) 実行結果

テストコードを実行すると、いずれか

  • pass (全て通る・成功)

  • fail (1つでも失敗・落ちる)

用語紹介(2/2) 値

かしこまった書き方
>>> actual = fizzbuzz(15)  # テスト対象を実行した値 
>>> expected = "FizzBuzz"  # 期待結果
>>> actual == expected
True

テストコードがあると

actual == expected簡単に確認 できる

actual

expected

fizzbuzz(3)

"Fizz"

fizzbuzz(5)

"Buzz"

fizzbuzz(15)

"FizzBuzz"

不安は退屈に変わる

  • 実装中、仕様を満たす 動作するコード であると確認できる🙌

  • 書き換える際も、おかしくしていたら気付ける 🙌(回帰テスト

書くコードは増えている、けれど

  • 実装に加えてテストコードも書く

  • でも、デメリット << メリット だと思うから、📣練習して書けるようになるんだ!

🥟テストコードはPythonの力をつける下地(N=1)

  • テストコードにより、「この実装は仕様を満たす 動作 するコード」と 確認 できる

  • 新しく知った文法を試して書き換えるとき、 って振る舞いを変えてしまっても 気づける

お品書き

  1. テストコードが書けるメリット

  2. doctestの使い方

  3. pytestの使い方

テストコードをどう書くか Part 1/2

doctest

関数のdocstring

クラス、関数、モジュールの最初の式である文字列リテラル

オブジェクトのドキュメントを書く標準的な場所

https://docs.python.org/ja/3/glossary.html#term-docstring

ドキュメンテーション文字列 とも

docstringの例

def fizzbuzz(number: int) -> str:
    """FizzBuzzゲームを解く関数(:1行要約)

    ...(後述)...
    """

Python対話モードの 実行例をdocstringに書く

def fizzbuzz(number: int) -> str:
    """FizzBuzzゲームを解く関数

    >>> fizzbuzz(1)
    '1'
    >>> fizzbuzz(3)
    'Fizz'
    """

対話モードの実行例を テストとして実行

.
└── fizzbuzz.py
  • python -m doctest fizzbuzz.py

実行結果の確認(-v オプション)

$ python -m doctest fizzbuzz.py -v
Trying:
    fizzbuzz(1)
Expecting:
    '1'
ok

4 passed and 0 failed.
Test passed.

関数のdocstringに限らず使えます

nikkieはテキストファイル(特にreStructuredText)で頻繁に使用

  • 書籍執筆

  • 発表資料 作成(本資料含む)

利用シーン:ライブラリのドキュメントにも

  • 例:scikit-learn

>>> from sklearn.metrics import f1_score  
>>> f1_score(y_true, y_pred, average='macro')  
0.26...

https://scikit-learn.org/stable/modules/generated/sklearn.metrics.f1_score.html

doctestの注意点

def fizzbuzz(number: int) -> str:
    """FizzBuzzゲームを解く関数

    >>> fizzbuzz(3)
    "Fizz"

    """

文字列に ダブルクォート を使ったらテストがfail😱

$ python -m doctest fizzbuzz.py
Failed example:
    fizzbuzz(3)
Expected:
    "Fizz"
Got:
    'Fizz'

***Test Failed*** 1 failures.

焦点:対話モードの出力結果として一致するか

  • 対話モードでは 文字列 は基本 シングルクォート で囲まれる

>>> "Fizz"
'Fizz'
  • doctestでも文字列はシングルクォートにする必要がある

対話モードは repr 関数の返り値

repr() 関数はインタープリタに読める(略)表現を返すためのもの

https://docs.python.org/ja/3/tutorial/inputoutput.html#fancier-output-formatting

repr 関数の返り値であることを利用した例 🏃‍♂️ (skip)

class Awesome:
    """
    >>> Awesome("PyCon APAC")
    Awesome('PyCon APAC')
    """

    def __init__(self, string: str) -> None:
        self.string = string

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}({self.string!r})"

(クラスのdocstringでdoctestの例にもなってます)

doctestから見るテストコードの構成要素

def fizzbuzz(number: int) -> str:
    """FizzBuzzゲームを解く関数

    >>> fizzbuzz(3)
    'Fizz'
    """
  • 実行例を書くだけで、関数に ある値を入力したときの出力 を検証できた

  • 3A という見方を導入

3A

  • Arrange 準備

  • Act 実行

  • Assert 検証

https://xp123.com/articles/3a-arrange-act-assert/

3Aで見るdoctest

※コメントを使って説明するため、対話モードで示します

>>> number = 3
>>> fizzbuzz(number)
'Fizz'

Arrange

テストの 準備 (データの用意など)

>>> number = 3
>>> fizzbuzz(number)
'Fizz'

Act

テスト対象の関数を 実行

>>> number = 3
>>> fizzbuzz(number)
'Fizz'

Assert

実行結果が期待値と等しいかを 検証

>>> number = 3
>>> fizzbuzz(number)
'Fizz'

第4のA:Annihilate 🏃‍♂️ (skip)

🥟doctest まとめ

  • 対話モードの 実行例を、docstringに書くだけ

  • python -m doctest にPythonファイルを渡してテスト実行

  • テストコードの一歩目として非常にオススメです

閑話休題🍵 お前、誰よ 続

関わっているコミュニティの ポスター @20F

  • Start Python Club (#stapy)

  • 読書py

お品書き

  1. テストコードが書けるメリット

  2. doctestの使い方

  3. pytestの使い方

テストコードをどう書くか Part 2/2

pytest

pytestで書くテストは 3 ステップ

  1. テストコードのファイルを作る

  2. テストコードとして、関数を書く

  3. assert文

Step1 pytestの規則に従った ファイル

  • test_ で始まるPythonファイルを作成

.
├── fizzbuzz.py
└── test_fizzbuzz.py

Step2 pytestの規則に従った 関数

  • test_ で始まる関数を書く

test_fizzbuzz.py
def test_3の倍数のときはFizzを返す():
    ...

Step3 Pythonの assert文

  • assert

  • 式が True と評価されるかを検証

https://docs.python.org/ja/3/reference/simple_stmts.html#the-assert-statement

assert文の例 1/2

>>> 1 == 1
True
>>> assert 1 == 1

assert文の例 2/2

>>> 1 == 2
False
>>> assert 1 == 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

3Aでとらえるpytest

test_fizzbuzz.py
def test_3の倍数のときはFizzを返す():
    number = 3  # Arrange
      # Act & Assert (actual == expected)
    assert fizzbuzz(number) == "Fizz"

テスト実行 pytest -v

.
├── fizzbuzz.py
└── test_fizzbuzz.py
$ pytest -v
============================= test session starts ==============================

collected 5 items

============================== 5 passed in 0.01s ===============================

failしたテスト(15でFizzを返した)

test_fizzbuzz.py::test_15の倍数のときはFizzBuzzを返す FAILED             [ 20%]

=================================== FAILURES ===================================
__________________________ test_15の倍数のときはFizzBuzzを返す ___________________________

    def test_15の倍数のときはFizzBuzzを返す():
>       assert fizzbuzz(15) == "FizzBuzz"
E       AssertionError: assert 'Fizz' == 'FizzBuzz'
E         - FizzBuzz
E         + Fizz
  • assert文だが、 なぜAssertionErrorかが分かりやすい

pytestは assert文を拡張

  • テストコードに使うのはassert文だけと 簡単

  • failしたテストの理由が 分かりやすい

pytestで docstringの対話例も 実行できる🏃‍♂️

tips (1/2) パラメタ化テスト

def test_3の倍数のときはFizzを返す():
    ...

3の倍数ならFizz

number取りうる値は複数

  • 3

  • 6

  • 9

個別にテストの関数を書く?

def test_3の倍数のときはFizzを返す_3の場合():
    ...

def test_3の倍数のときはFizzを返す_6の場合():
    ...

@pytest.mark.parametrize を使おう

@pytest.mark.parametrize("number", [3, 6])
def test_3の倍数のときはFizzを返す(number):
    assert fizzbuzz(number) == "Fizz"

1つの関数、複数のテストケース

$ pytest -v

test_fizzbuzz.py::test_3の倍数のときはFizzを返す[3] PASSED               [ 40%]
test_fizzbuzz.py::test_3の倍数のときはFizzを返す[6] PASSED               [ 60%]

個別に書いたのと同じ 結果が得られる

tips (2/2) モック

やや発展的話題(いまは分からなかったとしても大丈夫)

複数の処理を呼び出す実装のテストコードを書く

  • テストを書きたい関数 foo

  • 処理A -> B -> Cの順で呼び出し

def foo():
    a_func(42)
    b_func("ham", "egg")
    c_func()

どうテストコードを書くか?

  • 推し:呼び出される処理を ニセモノ(=モック)に置き換え てテスト

  • 全ての処理を通したテストも書ける

モックを使ったテスト

  • foo 関数で呼び出す各処理をテストにおいて 何もしない (=モック)に置き換える

  • モックは呼び出され方を記憶 している

モックを使ったテストの例

@patch("test_with_mock.c_func")
@patch("test_with_mock.b_func")
@patch("test_with_mock.a_func")
def test_foo(a_func, b_func, c_func):
    foo()

    a_func.assert_called_once_with(42)
    b_func.assert_called_once_with("ham", "egg")
    c_func.assert_called_once_with()
  • 処理の 呼び出しを検証

  • 処理A,B,C自体はいずれも別途、徹底的に検証

モックの使い所

  • 時間のかかる関数(テストの実行時間が伸びる)

  • 外部と通信する関数(通信エラーでテストが落ちうる)

  • 出力が変わる関数(例:random)

🥟pytest まとめ

  • test_ で始まるファイル・ test_ で始まる関数・ assert文

  • tips: パラメタ化 & モック

  • テストに慣れてきたらぜひ試してみてください!

まとめ🌯 練習してテストを書けるようになるんだ

  • テストを書くと、動作する? 間違えてない?という不安は 退屈 に変わる

  • 関数の呼び出しと返り値を docstringに書くだけ で、doctestでテストできる!(一歩目)

  • (拡張された)assert文をはじめ、 テストコードが書きやすいpytest もぜひ!

pytestはまだまだ序の口🏃‍♂️ (skip)

ご清聴ありがとうございました

好きとか嫌いとかはいい、練習してテストを書けるようになるんだ

Practice, practice, practice!!!

References

Appendix

お前、誰よ(詳細版)

補足コンテンツ

プロジェクトにおけるpytest

  • テストコードは tests ディレクトリの下にまとめるのが一般的

.
├── hayasaka
└── tests
    ├── __init__.py
    └── test_core.py

https://github.com/ftnext/hayasaka/tree/0.2.0

クラスも書けるなら、pytestで書くテストを 構造化 できる

class Test_FizzBuzz数列と変換規則を扱うFizzBuzzクラス:
    class Test_convertメソッドは数を文字列に変換する:
        class Test_3の倍数のときは数の代わりにFizzに変換する:
            def test_3を渡すと文字列Fizzを返す(self, fizzBuzz):
                assert "Fizz" == fizzBuzz.convert(3)

https://github.com/ftnext/tddbc-fizzbuzz/blob/8ea856c4c59780837410a44a858368047269f3c8/tests/test_fizzbuzz.py#L11-L15

15分版では割愛した、標準ライブラリ unittest

テストを書いているあなたへ

拙ブログ テストを書くようになったあなたと語りたいトピック集

タイトルの秘密

  • Twitterで見かけた ちよ父 の画像

    • 元は「トマトを食べるんだ」🍅

亜種

EOF