Pytestを使って自分なりに解説(mock/parametrize)

# 前提
* pytestでmockを使ってUTする際にパターンを忘れてggrことが多いので自分用に雑多に残しておきます。

# mockの予備知識
* 外部APIからのレスポンスや、まだ未定義の関数や変数やクラスに対してmock化することで、疑似的に関数から値を受け取る処理を作ることができる。
* mock化する関数自体は関数名と、passのみやreturnのみでもOK
 → ただし、mock化して値を上書きする為のガワは必要なのでモジュール作成や関数は別途作成が必須

# 実施準備

①Pytestは標準モジュールではないので pip にてインストールが必要
```
 $ pip install pytest pytest-mock
```

②各プロジェクト・モジュールの準備
```
└── sandbox/
      ├── calc.py
      └── test/
            └── test_calc.py
```

③テスト実施する階層に移動
```
$ cd sandbox
```

④pytestは python -m でpytestを指定してUT実行
※python3 -m でも可
```
# 標準出力なし
$ python -m pytest test/test_calc.py

# 標準出力あり
$ python -m pytest -s test/test_calc.py
```

# 関数ベースの変数の値をmock化
```calc.py 
# この変数をmock化する True -> False に変更
result_flg = True

def calc():
    # このまま実行すると、ここの分岐に入ることはできない
    if not result_flg:
        return {"result_flg": False}
```

```test_calc.py
import pytest
from unittest.mock import patch
from calc import calc

# 第一引数=変数までのパスと変数名、第二引数=変更したい変数の値を設定
@patch("calc.result_flg", False)
def test_calc(mocker):
   
    # 関数を実行
    result = calc()

    # assertして実際に変数がFalseになっていることを確認
    assert result == {"result_flg": False}
    
```


# 関数ベースの関数をmock化(正常UTパターン)
```calc.py
# この関数をmock化する
def calc_exec(num): 
    pass

def calc(param_dict):
    total = 0
    for i in range(1,4):
        # 関数をmock化することで、変数が受け取る値を変更できる
        result = calc_exec(param_dict["num"])
        total += result
    return total

```

```test_calc.py
from calc import calc

def test_calc(mocker):
    # 第一引数=関数までのパス、第二引数=return_valueかside_effectを設定
    # [return_value= は固定値での返却]、[side_effect= は柔軟な値返却や例外の返却]
    mocker.patch(
        "calc.calc_exec",
        # 同じ関数名を複数回呼び出した時の1~3回目(forループも含む)の値を第一～第三引数で柔軟に設定
        # → 1回目:4, 2回目:6, 3回目:8
        side_effect=[2*2, 3*2, 4*2,],
    )
    
    # mock化した後にテスト対象の関数を実行する
    result = calc(
        {
            "num": 2
        }    
    )
    
    # 値をassertする
    assert result == 18

```

# 関数ベースの関数をmock化(例外UTパターン)
```calc.py
# 自作の例外クラスを定義
class CalcZeroError(Exception):
    pass

# この関数をmock化する
def calc_exec(num):
    pass

def calc(param_dict):
    # 関数をmock化することで、変数が受け取る値を変更できる
    result = calc_exec(param_dict["num"])
    if result == 0:
        raise CalcZeroError("failed to calc")
```

```test_calc.py
import pytest
from calc import calc

def test_calc(mocker):
    # 第一引数=関数までのパス、第二引数=return_valueかside_effectを設定
    # [return_value= は固定値での返却]、[side_effect= は柔軟な値返却や例外の返却
    mocker.patch(
        "calc.calc_exec",
        return_value=0,
    )
   
    # mock化したあとに、下記処理内でException例外が発生するかを判定
    with pytest.raises(Exception) as e:
        result = calc(
            {
                "num": 2
            }    
        )
    # 例外発生した際のメッセージを比較
    assert str(e.value) == "failed to calc" 
```

# parametrizeを使って関数ベースの関数のUTの値を柔軟に変更
* parametrizeを使用する流れ
  1. paramitrizeで使用するためのリスト内タプルを作成(params)
  2. タプル内の引数を必要な数作成し、必要な値を記述(関数呼び出しを記載するのも可)-> 今回は4つ
  3. 使用するテスト関数上部にデコレータでparametrizeと任意の変数名を付与
  4. testの引数に3.の変数名を受け取る
  5. test内で柔軟に値を変えたい箇所に引数名を記載する
  6. 下記テストコード内の★1~3の順で1回ずつテストが走る(計3回)
  7. 今回のケースでは3回分可変で4つの値が変数に代入されてテスト判定
```calc.py
def calc(a, b, c):
    return a + b + c

```

```test_calc.py
import pytest
from calc import calc

# parametrizeでテストする値を3パターン リスト内タプルで作成
params = [
    # 1ケース目 param_a, param_b, param_c, result の順で設定
    (1, 2, 3, 6), # ★1
    # 2ケース目 param_a, param_b, param_c, result の順で設定
    (3, 4, 7, 14), # ★2
    # 3ケース目 param_a, param_b, param_c, result の順で設定
    (4, 5, 6, 15), # ★3
]

@pytest.mark.parametrize(
    "param_a, param_b, param_c, result",
    params)
# paramsのリストから4つの値を受け取る
def test_calc(param_a, param_b, param_c, result):
    ans = calc(param_a, param_b, param_c)
    # 3回分のケースを1回ずつ比較
    assert ans == result
```