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


##  1. 単体テスト（Unit Test）の基本

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

### `math_utils.py`

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

### `test_math_utils.py`

```python
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` になるべきです。


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

```bash
pytest test_math_utils.py
```

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

```bash
============================= test session starts =============================
collected 1 item

test_math_utils.py .                                                   [100%]

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

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

```python
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` を使うと便利です。

```python
@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` であるべき」という意味。

```python
def test_is_even(n, expected):
    assert is_even(n) == expected
```

- `n` と `expected` が、上の `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` を使います。

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

```python
# 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()]
```

### テスト＋フィクスチャ：

```python
# 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の呼び出しをテストしたいとき
- データベース操作など、**本当に実行したくない処理**を偽装したいとき
- **関数が呼ばれたか・何回呼ばれたか・どんな引数で呼ばれたか**をチェックしたいとき


### 本体コード：

```python
# 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()` されたくないので **モック（偽の関数）** にすり替えます
### テストコード：

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