Pythonのpytest-cov

Python学習【365日チャレンジ!】185日目のマスターU(@Udemy11)です。

いつ頃どんな魚が釣れたのかチェックするために釣り日記をつけているのですが、8月はあまり釣りに行っていないようです。

夕方に少し時間ができてので、そろそろ太刀魚が釣れるかな~と思って近くのポイントに行ってみたところ釣り人は皆無。

いや~な予感を感じながら、暗くなるまで投げ続けましたが、結局最後までなんの反応もありませんでした。

いつも釣果をインスタにあげている九州の方の友人は絶好調のようで、指4本の太刀魚を7本もあげて、どや顔してるのに、こちらではまだちょっと早いんでしょうかね。

それでは、今日もPython学習をはじめましょう!

昨日の復習

昨日は、pytestの独自fixtureを学習しました。

pytestで最初に読み込むconftest.pyに、どのテストでも共通で使う独自のfixtureを定義すれば、それぞれのテストファイルに個別に記述する必要がなくなります。

また、yieldを使うことでテストの前後になにか処理をさせることができました。

fixtureはpytestだけでなく、他のテストフレームでも活用されているので、しっかりとマスターしておきましょう。

今日は、テストのカバー率を確認する学習します。

pytest-covのインストール

実行するテストがどの程度テストされるファイルのコードをカバーできているかを知ることはテストをする上で必要です。

テストしたといってもメインファイルの50%しかできていなければ、残りの50%がバグにつながる可能性があります。

テストファイルがどの程度のコードをカバーしているかをチェックできるのが、今日学習するpytest-covです。

今回は使いませんが、PyPiの公式ドキュメントによると分散テスト(distributed test)のプラグインであるpytest-xdistも合わせてインストールしておきます。

pip install pytest-cov pytest-xdist

ターミナルで上記のようにpipインストールします

毎度のことですが、AnacondaでPythonをインストールした場合は、どちらもすでにインストールされているかと思います。

関連ファイル

まずは、テストのカバー率を確認するメインのファイルとテストファイルを確認しましょう。

calculation.py

import os

class Cal(object):
    def add_and_double(self, x, y):
        if type(x) is not int or type(y) is not int:
            raise ValueError
        result = x + y
        result *= 2
        return result

    def save(self, dir_path, file_name):
        if not os.path.exists(dir_path):
            os.mkdir(dir_path)
        file_path = os.path.join(dir_path, file_name)
        with open(file_path, 'w') as f:
            f.write('test')

test_calculation.py

import os

import calculation

class TestCal(object):

    @classmethod
    def setup_class(cls):
        cls.cal = calculation.Cal()
        cls.test_file_name = 'test.txt'

    def test_add_and_double(self):
        assert self.cal.add_and_double(1, 3) == 8

    def test_save(self, tmpdir):
        self.cal.save(tmpdir, self.test_file_name)
        test_file_path = os.path.join(tmpdir, self.test_file_name)
        assert os.path.exists(test_file_path) is True

pytest-covを使ってターミナルからコマンドを実行することで、calculation.pyのコードのうち、test_calculation.pyがテストするカバー率がわかります。

ターミナルから実行

テストのカバー率をチェックするには、ターミナルから次のコードを実行します。

pytest test_calculation.py --cov=calculation --cov-report term-missing

このコマンドは、テストファイルをpytestで実行して、メインファイルのコードのカバー率、実行されないコード行を表示します。

実行結果

Pythonのpytest-cov

メインファイルの6行目と13行目の2つのコードがテストされてなくて、カバー率は86%になっています。

テストコードを追加

テストされていないコードをみてみましょう。

まず、6行目ですが、add_and_double()の引数が整数以外だったときのraise ValueErrorが動作するかどうかがテストされていないということです。

次の13行目は、ディレクトリパス(dir_path)が存在していなかった場合、そのディレクトリパスを作成するコードです。

この2つをテストするコードを追加します。

次のコードはraise ValueErrorをテストするコードです。

    def test_add_and_double_raise(self):
        with pytest.raises(ValueError):
            self.cal.add_and_double('1', '1')

関数add_and_doubleの引数に文字列が代入されるので、前回テストされなかったメインファイルの6行目がテストされることになります。

次にディレクトリが存在しているかどうかをチェックするコードですが、まずshutilをインポートして、@classmethodsetup_classに次の【ディレクトリ名を定義する変数】とteardownで【作成したディレクトリを削除するコード】を追加します。

#shutilをインポート
import shutil

#ディレクトリ名を定義する変数
        cls.test_dir = '/tmp/test_dir'

#作成したディレクトリを削除するコード
    def teardown_class(cls):
        if os.path.exists(cls.test_dir):
            shutil.rmtree(cls.test_dir)

次に、ディレクトリパスが存在しないときにディレクトリパスを作成するコードをテストするコードを追加します。

    def test_save_no_dir(self):
        self.cal.save(self.test_dir, self.test_file_name)
        test_file_path = os.path.join(self.test_dir, self.test_file_name)
        assert os.path.exists(test_file_path) is True

@classmethodで定義した変数test_dirtest_file_nameを使って作成したディレクトリパス(test_file_path)の存在をテストしています。

これで、前回はスキップされていた13行目のディレクトリが存在しなかった場合のコードがテストされます。

再チェック

コードができたら、ターミナルからもう一度次のコードを実行してみましょう

pytest test_calculation.py --cov=calculation --cov-report term-missing

実行結果

Pythonのpytest-cov

前は2つのコードがテストされずにカバー率は86%でしたが、今回は新しく追加したテストが実行されて、カバー率が100%になっています。

どこまでのカバー率が必要?

最初にカバーできていなかった下記のメインファイルの12行目と13行目のコードのテストは、条件(ディレクトリが存在する場合)によってスキップされます。

        if not os.path.exists(dir_path):
            os.mkdir(dir_path)

なので、13行目のos.mkdiros.makdirのようにスペルミスしていたとしても今回の最初のテスト段階では、pytestを実行してもテストに合格するので、バグとして残ってしまいます。

このようなことがあるので、カバー率を上げる必要がありますが、全てのテストを100%にしなければいけないわけではなく、カバー率を80%にするとか90%にするとかは、クライアント次第ということになります。

このあたりは経験を積み上げないとわからないみたいなので、学習を継続しつつ経験値を稼いでいきたいと思います。

それでは、明日もGood Python!