BFT名古屋 TECH BLOG

日々の業務で得た知識を所属するエンジニアたちがアウトプットしていきます。

【Python】【pytest】第三弾 つまづきポイント編

こんにちは!BFT名古屋支店の猫です。

先日初めてpytestを使用してみたので、学んだことを3記事に分けてまとめました。
今回は 第三弾 つまづきポイント編 として、私がテスト作成・実行時につまづいたポイントと対処法を3つご紹介します。
pytestをこれから使う/最近使い始めた方の参考になればと思います。

第一弾 導入~カバレッジ測定編 はこちら
第二弾 カバレッジを上げるためにやったこと編 はこちら

環境

  • Ubuntu 18.04.5 LTS (WSL1)
  • Python 3.8.10
  • pytest実行やカバレッジ測定に必要なライブラリ
    • pytest 6.2.5
    • coverage 5.5
    • pytest-cov 2.12.1
    • pytest-mock 3.6.1

テスト対象コード

例として、引数の2整数に対して四則演算した結果を返すプログラムを作りました。

●Arithmetic.py

import json
import os
from datetime import datetime as dt

from lib import ErrorClass as ec

### 現在日時を取得する関数
def get_date():
    return dt.now().strftime('%Y-%m-%d %H:%M:%S')

### 引数で指定された演算を行う関数
def calc(var1:int, var2:int, method:str):
    if method == 'add':
        return var1 + var2
    elif method == 'sub':
        return var1 - var2
    elif method == 'mult':
        return var1 * var2
    elif method == 'div':
        return var1 / var2

###
### メイン関数:引数で指定された2整数の四則演算の結果を出力する
###
def main(var1:int, var2:int):

    result = dict()
    result['date'] = get_date()
    try:
        if type(var1) != int or type(var2) != int:
            raise ec.TypeError

        result['sum'] = calc(var1, var2, 'add')
        result['diff'] = calc(var1, var2, 'sub')
        result['prod'] = calc(var1, var2, 'mult')
        result['quot'] = calc(var1, var2, 'div')

    except ec.TypeError as e:
        result['error_code'] = e.__class__.__name__
    
    except:
        result['error_code'] = 'MyException'

    # 結果書き込み処理
    if not os.path.isfile('result.json'):
        with open('result.json', 'w') as fp:
            json.dump(result,fp)

    return result

●ErrorClass.py

# エラークラスの定義
class TypeError(Exception):
    def error_code(self):
        pass

つまづきポイント

①パスが通らない

pytestに初めて触ったときは永遠に「そんなテスト/モジュールはねえ!」というエラーが出てました。悲しかったです。
対処法と謳っておいて申し訳ないのですが、Google先生で調べまくって色々試して解決しました。 おすすめ確認ポイントとしては以下三点です。

  • PYTHONPPATHが正しく設定されているか
    ここまで連れてくればいいはずなのになぜ…?というときはとりあえず一階層下までご案内。
    設定して、コマンドで確認してもちゃんと設定されているのになぜ!というときはとりあえず再起動。
  • pytest.iniの書き方は正しいか
  • __init__.pyは正しく配置されているか

ちなみにVSCode上でもpytestを実行することができます。(ステップ実行や変数値の表示ができるのでとても便利です!)
が、こちらもパスが通らなくてとても苦労しました。
四苦八苦して何とか実行できるようになったのですが詳しく理解できていません・・・。
このあたりについてはまたの機会に記事にしたいと思います。

②try-exceptでキャッチされる例外はpytest.raisesではキャッチできない

例えばArithmetic.main()に引数(2,0)を渡したとします。すると除算で例外が起こるはずです。
そのため私は最初、以下のようなテストコードを記述していました。

# FAILになるテスト
def test_Exception_fail():
    with pytest.raises(Exception):
        Arithmetic.main(2, 0)

しかしこのテストを実行すると、結果がFAILになってしまいます。
このような↓エラーメッセージが出ます。

Failed: DID NOT RAISE <class 'Exception'>

なんでやねん!例外起こるやろ!と私の内なる関西人が文句を言っていたのですが
理由は簡単で、main()内のtry-exceptが、発生した例外をキャッチしていたからでした。
pytest.raisesは異常"終了"のテストに用いるものなので、例外が発生しても正常に終了する場合は当てはまらないということです。

以下のように記述することで、無事にPASSEDになりました。

# PASSEDになるテスト
def test_Exception_success():
    res = Arithmetic.main(2,0)
    assert res['error_code'] == 'MyException'

③mocker.patchでローカル変数はパッチできない

※これはもしかしたら私が解決方法にたどり着けていないだけかもしれません。また進展があれば更新します。

カバレッジを上げるためにしたこと」の中で、mocker.patchを用いて、メソッドの返り値を制御する方法をご紹介しました。
モックの便利さに魅了されてしまった私は、返り値とか面倒なことやってないで変数の値をパッチすればよいのでは?と思い至り「pytest mock 変数」と調べました。
するとそれっぽい記事が出てくるではありませんか!
鼻息を荒くしながら、見よう見まねで以下のようなテストコードを記述しました。

# ローカル変数をパッチしようと試みているテスト
def test_mock_local_var_fail(mocker):
    mocker.patch.object(Arithmetic,'result[\'date\']', value='2022-01-01 01:01:01')
    res = Arithmetic.main(1, 1)
    assert res['date'] == '2022-01-01 01:01:01'

しかし結果はFAILD。このような↓エラーメッセージが出ます。

AttributeError: <module 'Arithmetic' from '/mnt/c/Users/****/project_root_dir/code_dir/Arithmetic.py'> does not have the attribute "result['date']"

試行錯誤の末「パッチできるのはグローバル変数だけで、ローカル変数はできないらしい」という結論に至りました。
そのため対処法としては、「ローカル変数の値はメソッドの返り値でなんとかする」です。解決してないですね。すみません。

おわりに

3記事に渡ってpytestに関するご紹介をしました。
ツールを上手に使いこなしつつ、よりよいテスト・よりよい設計についても学んでいきたいです。

ここまで読んでくださりありがとうございました!