pytest + pytest-mockを使ったモック関数のテスト実装

# はじめに

本記事では、`pytest` と `pytest-mock` を使用して、関数内部の処理と戻り値をテストする方法を解説します。


# 環境
- Python: Python 3.12
- pytest: 8.2.2
- pytest-mock：3.14.0


`pytest`、`pytest-mock`は以下のコマンドでインストールしておきます。

```
pip install pytest pytest-mock
```
https://pypi.org/project/pytest-mock/

https://pypi.org/project/pytest/

# 本記事でやること
関数の戻り値のテストと内部処理のモック化を、正常系・異常系のパターンで実装します。


# テスト対象のコード
例としてユーザー登録時の年齢チェックとメッセージ作成を行う簡単な関数を作成してみます。

```python:user_service.py
# 年齢確認
def validate_age(age):
    return 0 <= age <= 120

# メッセージ
def format_message(name):
    return f"こんにちは、{name}さん"

# 年齢が有効な場合のみメッセージを返す
def register_user(name, age):
    if not validate_age(age):
        return "年齢が不正です"
    
    message = format_message(name)
    return message
```

# テストケース
以下の4つのパターンをテストします。

| テスト名（関数名） | テストの対象 | テストのケース | テスト内容 | 期待する結果 |
|---------|------------|--------------|------------|------------|
| test_register_user_internal_calls_success | 内部関数 | 正常系 | validate_ageとformat_messageの呼び出しを検証 | ①validate_age(20)が1回呼び出される<br>②format_message("テスト")が1回呼び出される |
| test_register_user_internal_calls_invalid_age | 内部関数 | 異常系 | validate_ageの呼び出しとformat_messageの未呼び出しを検証 | ①validate_age(150)が1回呼び出される<br>②format_messageは呼び出されない |
| test_register_user_success | 戻り値 | 正常系 | 有効な年齢（20歳）でユーザー登録 | 戻り値が "こんにちは、テストさん" |
| test_register_user_invalid_age | 戻り値 | 異常系 | 不正な年齢（150歳）でユーザー登録 | 戻り値が "年齢が不正です" |

# モックテストで使用する主な機能
本テストにおいて、内部関数の呼び出しをテストする際に使用する主な機能は以下の通りです。

1. `return_value`
   - モック化した関数の戻り値を設定
   - 例：`mock_validate.return_value = True`

2. `assert_called_once_with(引数)`
   - 関数が指定した引数で1回だけ呼ばれたことを確認
   - 例：`mock_validate.assert_called_once_with(20)`

3. `assert_not_called()`
   - 関数が1回も呼ばれていないことを確認
   - 例：`mock_format.assert_not_called()`


# テストコード
```python:test_user_service.py
import pytest
from user_service import register_user


# 内部関数の呼び出しテスト：正常系
def test_register_user_internal_calls_success(mocker):
   """
   - validate_age(20)が1回呼ばれること
   - format_message("テスト")が1回呼ばれること
   """
   mock_validate = mocker.patch('user_service.validate_age', return_value=True)
   mock_format = mocker.patch('user_service.format_message', return_value="こんにちは、テストさん")
   
   register_user("テスト", 20)

   # validate_ageが20という引数で1回呼ばれたか
   mock_validate.assert_called_once_with(20)
   # format_messageが"テスト"という引数で1回呼ばれたか
   mock_format.assert_called_once_with("テスト")


#  内部関数の呼び出しテスト：異常系
def test_register_user_internal_calls_invalid_age(mocker):
   """
   - validate_age(150)が1回呼ばれること
   - format_messageは呼ばれないこと
   """
   mock_validate = mocker.patch('user_service.validate_age', return_value=False)
   mock_format = mocker.patch('user_service.format_message')
   
   register_user("テスト", 150)

   # validate_ageは150という引数で1回呼ばれる
   mock_validate.assert_called_once_with(150)
   # format_messageは呼ばれない
   mock_format.assert_not_called()


# 戻り値の確認：正常系（有効な年齢）
def test_register_user_success():
   result = register_user("テスト", 20)
   assert result == "こんにちは、テストさん"


# 戻り値の確認：異常系（不正な年齢）
def test_register_user_invalid_age():
   result = register_user("テスト", 150)
   assert result == "年齢が不正です"
```

# テストの実行

```bash
pytest test_user_service.py -v
```

上記コマンドで実行した結果は下記のようになります。

```
============================== test session starts ===================================
～～～
collected 4 items                                                                                                     

test_user_service.py::test_register_user_success PASSED                       [ 25%] 
test_user_service.py::test_register_user_invalid_age PASSED                   [ 50%] 
test_user_service.py::test_register_user_internal_calls_success PASSED        [ 75%] 
test_user_service.py::test_register_user_internal_calls_invalid_age PASSED    [100%] 
============================== 4 passed in 0.02s =====================================
```