0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonのPytestを使ったテストの基本

Posted at

1. 単体テスト(Unit Test)の基本

まずはシンプルな関数を用意して、偶数かどうかを判定します。

math_utils.py

def is_even(n: int) -> bool:
    return n % 2 == 0

test_math_utils.py

from math_utils import is_even

def test_is_even():
    assert is_even(2)
    assert not is_even(3)
    assert is_even(100)
  • def test_is_even():
    → テスト関数を定義しています。先頭にtestと付けることでpytestが、テストと認識します。

  • assert is_even(2)
    is_even(2)True を返すことを確認します。期待どおりならOK。

  • assert not is_even(3)
    is_even(3)False を返すことを確認します。not をつけているので、「False を期待している」ことになります。

  • assert is_even(100)
    → 100 も偶数なので、True になるべきです。

ターミナルで以下のコマンドを実行:

pytest test_math_utils.py

成功すればこんな感じで表示されます:

============================= test session starts =============================
collected 1 item

test_math_utils.py .                                                   [100%]

============================== 1 passed in 0.01s ==============================

2. パラメータ化されたテスト

import pytest
from math_utils import is_even

@pytest.mark.parametrize(("n", "expected"), [
    (1, False),
    (2, True),
    (3, False),
    (10, True),
])
def test_is_even(n, expected):
    assert is_even(n) == expected

同じ関数に対して、いろんな入力と結果をまとめてテストしたい時は @pytest.mark.parametrize を使うと便利です。

@pytest.mark.parametrize(("n", "expected"), [
    (1, False),
    (2, True),
    (3, False),
    (10, True),
])

この部分で 「テストデータの一覧」 を定義しています。

  • ("n", "expected")
    → これは各テストに渡される引数の名前です。

  • [(1, False), (2, True), ...]
    → それぞれのタプルが「テストケース」です。
    たとえば (1, False) は「is_even(1) の結果は False であるべき」という意味。

def test_is_even(n, expected):
    assert is_even(n) == expected
  • nexpected が、上の parametrize のリストから自動で渡されます。
  • is_even(n) の結果が expected と一致しているかをチェックします。

実際には以下のように 書くコードは1つなのに4つのテスト が実行されます:

  1. assert is_even(1) == False
  2. assert is_even(2) == True
  3. assert is_even(3) == False
  4. assert is_even(10) == True

3. フィクスチャ(前処理・後処理)

「テストに必要な準備(前処理)」や「クリーンアップ(後処理)」を行いたい時は @pytest.fixture を使います。

下記の例では、ファイルの中身を読み取って、その中身が期待通りの数値かどうかを比較する」 テストを行います。

ファイルの読み込み関数:

# file_utils.py

from typing import List

def read_numbers(path: str) -> List[int]:
    with open(path) as f:
        return [int(line.strip()) for line in f.readlines()]

テスト+フィクスチャ:

# test_file_utils.py

import pytest
from file_utils import read_numbers

@pytest.fixture
def numbers_file(tmp_path) -> str:
    file = tmp_path / "numbers.txt"
    file.write_text("3\n1\n2\n")
    return str(file)

def test_read_numbers(numbers_file):
    assert sorted(read_numbers(numbers_file)) == [1, 2, 3]
説明
@pytest.fixture フィクスチャの定義。テストの前に呼ばれる準備関数です。
tmp_path pytest が自動で用意してくれる「一時的な空フォルダ」。テストが終われば自動で削除されます。
file = tmp_path / "numbers.txt" その中に "numbers.txt" というファイルを作る準備
file.write_text("3\n1\n2\n") ファイルの中身として「3 改行 1 改行 2」を書き込む
return str(file) ファイルのパスを文字列として返す

@pytest.fixtureを使う事で、

  • テストの中で直接ファイル操作しなくていい
  • 複数のテストで 使い回し できる
  • テスト後に 自動で片付けてくれる(tmp_path)
  • データベースの初期化・クラスのセットアップなどにも使える

4. モック(外部の処理を偽装する)

下記の状況の場合、pytest-mock を使ってモック化することで実現することができます。

  • 外部APIの呼び出しをテストしたいとき
  • データベース操作など、本当に実行したくない処理を偽装したいとき
  • 関数が呼ばれたか・何回呼ばれたか・どんな引数で呼ばれたかをチェックしたいとき

本体コード:

# messenger.py

def send_message(msg: str):
    log_message(msg)

def log_message(msg: str):
    print(f"[LOG] {msg}")

目的としては、

  • 関数 send_message() の中で log_message() を呼んでいるけど、本当に log_message("Test") が呼ばれたか?を確認したい
  • でも実際に print() されたくないので モック(偽の関数) にすり替えます

テストコード:

def test_send_message(mocker):
    log_mock = mocker.patch("messenger.log_message")
    from messenger import send_message

    send_message("Test")

    log_mock.assert_called_once_with("Test")

mocker.patch("messenger.log_message")

  • messenger.log_messageモック(偽の関数)に差し替え ます。
  • つまり print() は実行されず、呼び出し記録だけが残る。
  • log_mock というモックオブジェクトが返ってくる。

from messenger import send_message

  • patch の後でインポートすることが大事
  • なぜなら、send_message の中で呼ばれる log_message がすでにモックに置き換わっている必要があるから。

send_message("Test")

  • これで log_message("Test") が呼ばれる(モックになってる)

log_mock.assert_called_once_with("Test")

  • 1回だけ」「引数が "Test" だった」という条件で呼ばれたかチェック
  • これが間違っているとテストは失敗します

まとめ

Pytestを使えば、シンプルな関数からファイル操作、標準出力、外部依存の検証まで、さまざまなテストが簡単に書けます。
上手に活用する事で安全安心に開発を進めていくことができます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?