BFT名古屋 TECH BLOG

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

【Python】【pytest】第一弾 導入~カバレッジ測定編

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

先日初めてpytestを使用してみたので、学んだことを3記事に分けてまとめました。
今回は 第一弾 導入~カバレッジ測定編 として、pytestを導入してカバレッジを測定するまでの手順をご紹介します。
pytestをこれから使う/最近使い始めた方の参考になればと思います。

pytestとは

pytestとは、Pythonコードをテストするためのツールです。
実行する関数や引数、返り値を設定して、想定通りにプログラムが動作するかを確認することができます。
docs.pytest.org

環境

  • 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

ディレクトリ構成

project_root_dir             … プロジェクトルートディレクトリ
├── lib
│   ├── ErrorClass.py    … Arithmetic.pyに呼ばれるファイル
│   └── __init__.py
├── code_dir
│   ├── __init__.py
│   └── Arithmetic.py          … テスト対象のファイル  
├── testcode_dir
│   └── test_Arithmetic.py     … テストコードが書かれたファイル
└── pytest.ini            … pytest実行に必要なファイル

テスト対象コード

例として、引数の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導入

①ライブラリのインストール

pipコマンドで、pytest実行とカバレッジ測定に必要なライブラリをインストールします。

$ pip install pytest coverage pytest-cov

②PYTHONPATHを通す

色々な方法があるかと思いますが、今回はホームディレクトリ配下の.bashrcに以下を追加しました。
pytestを実行するにはいくつかのディレクトリにパスを通す必要があったため、コロン(:)を使用して複数のディレクトリを指定しました。

export PYTHONPATH=/mnt/c/Users/****/project_root_dir:/mnt/c/Users/****/project_root_dir/code_dir

Windows版を利用される方は、ユーザー環境変数 "PYTHONPATH" を新規作成 > 値にフォルダのパスを登録して、再起動(大事!)してください。

③pytest.iniの作成

pytest.iniという名前のファイルを作成し、プロジェクトのルートディレクトリ直下に配置します。
pytest.iniでは、実行するテストコードを指定することができます。
使用しなくても実行はできますが、テストコードが増えた時に部分的に実行するのに便利です。

[pytest]
testpaths = /mnt/c/Users/****/project_root_dir/testcode_dir  … テストコードのパスを指定
python_files = test_Arithmetic.py                              … テストコードのファイル名を指定
python_functions = test_                                 … 「test_」で始まる関数名のテストを実行する

④__init__.pyの作成と配置

__init__.pyという名前の空ファイルを作成し、テスト対象のコードが配置されているディレクトリそれぞれに配置します。
今回の例ですと、Arithmetic.pyがあるcode_dirディレクトリだけでなく、ErrorClass.pyがあるlibディレクトリにも配置します。

⑤テストコードの作成

正常系と異常系で一つずつテストコードを用意しました。

import Arithmetic                      

# 正常系
def test_success():
    res = Arithmetic.main(1,2)         
    assert res['sum'] == 3       # 想定される結果を「assert <式>」で書く
    assert res['diff'] == -1

# 異常系
def test_TypeError():
    res = Arithmetic.main(1.5,1)       # 整数でない引数を渡しているため例外が起こる
    assert res['error_code'] == "TypeError"

⑥pytest実行&結果確認

pytestコマンドでテストを実行します。
以下の例では詳細な結果を表示するための-vオプションと、カバレッジを測定するための--covオプションを使用しています。
測定したカバレッジ$ coverage html <カバレッジ測定対象>を実行してhtml形式で保存することができます。

$ pytest -v --cov=Arithmetic
========================= test session starts =========================
platform linux -- Python 3.8.10, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- /home/****/.pyenv/versions/3.8.10/envs/prac_venv/bin/python3.8
cachedir: .pytest_cache
rootdir: /mnt/c/Users/****/project_root_dir, configfile: pytest.ini, testpaths: /mnt/c/Users/****/project_root_dir/testcode_dir
plugins: cov-3.0.0, mock-3.6.1
collected 2 items

testcode_dir/test_Arithmetic.py::test_success PASSED                  [ 50%]
testcode_dir/test_Arithmetic.py::test_TypeError PASSED                [100%]

---------- coverage: platform linux, python 3.8.10-final-0 -----------
Name               Stmts   Miss  Cover
--------------------------------------
code_dir/Arithmetic.py      33      4    88%
--------------------------------------
TOTAL                 33      4    88%


========================== 2 passed in 0.12s ==========================
$ coverage html Arithmetic.py

上記のコマンドを実行後 <プロジェクトルートディレクトリ>/htmlcov/Arithmetic_py.htmlを開くと、以下の画面が表示されます。
白くなっているのはテストにより実行された部分、赤くなっているのは実行されていない部分を表します。
この例だと、42~43行目と47~48行目が赤くなっているので実行されていないことが分かります。

f:id:bftnagoya:20220118204319p:plain
カバレッジ測定結果

おわりに

今回測定したカバレッジは 88% でした!
次回はカバレッジを100%に近づけるために実施した方法を4つご紹介します。

第二弾 カバレッジを上げるためにやったこと編 お楽しみに!

参考

rinatz.github.io

qiita.com

www.anypalette.com

pytest で単体テストの方法まとめ - Qiita

Python内のデフォルトパスを通す方法(Windows, Linux) - Qiita