Blogical

AWS/Salesforceを中心に様々な情報を配信していきます(/・ω・)/

VS Code で Python の開発環境を用意する【単体テスト編】

こんにちは、ロジカル・アーツの笹原です。

今回は、VS Code と pytest を使用した Python コードの単体テストの方法を紹介したいと思います。

はじめに

VS Code ではテストの実行はもちろんですが、テストに対してデバッグも行うことができます。さらに、拡張機能を使用してカバレッジを表示することも可能です。以下では、pytest を使用した基本的なテスト方法と、デバッグカバレッジの表示方法を取り上げます。

以下の手順で扱う環境は次の通りです:

単体テスト

この記事では pytest を使用します。以下のコマンドでインストールします*1

pip install pytest

テスト用サンプルコード

以下のコードをサンプルとして使用します。これを collatz.py としてワークスペース直下に作成します。

この項の以下の説明は VSCode で Python の開発環境を用意する【デバッグ編】 - Blogical にあるものとまったく同じです。既に読まれている方は読み飛ばしてもらって大丈夫です。

collatz.py

def collatz(n):
    """与えられた数を初期値として Collatz 数列を計算する。

    Args:
        n (int): 初期値。

    Raises:
        NotPositiveNumberError: 初期値が0以下の場合に発生する。

    Returns:
        tuple: 数列の長さと最大値のタプル。
    """
    seq = [n]
    if n <= 0:
        raise NotPositiveNumberError

    while n > 1:
        if is_even(n):
            n //= 2
        else:
            n = 3 * n + 1
        seq.append(n)
    return len(seq), max(seq)


def is_even(n):
    """与えられた数が偶数かどうか判定する。

    Args:
        n (int): 整数。

    Returns:
        bool: 偶数であれば `True` を返す。
    """
    return n % 2 == 0


class NotPositiveNumberError(Exception):
    """正の整数でない例外クラス。"""

    pass


if __name__ == "__main__":
    n = input("数字を入力してください:")
    try:
        length, max_num = collatz(int(n, base=10))
        print(f"数列の長さ:{length}")
        print(f"数列の最大値:{max_num}")
    except (NotPositiveNumberError, ValueError):
        print("正の整数を入力してください。")

このサンプルコードでは Collatz 予想に出てくるアルゴリズムを実装してます。すなわち、与えられた正の整数に対して偶数であれば 2 で割り、奇数であれば 3 倍して 1 を足すという操作を繰り返し行います*2。 コード中の collatz 関数は入力が 1 に到達したときの数列の長さと、取り得る値の最大値を返します。

例えば、入力が 13 のとき Collatz 数列は

 \displaystyle 13\to40\to20\to10\to5\to16\to8\to4\to2\to1

となり、数列の長さは 10、最大値は 40 です。

設定

VS Codecollatz.py を開いておきます。すると以下の画像のようなフラスコアイコンが表示されると思うのでクリックし、「Configure Python Tests」をクリックします。

「pytest」を選択します。

「. Root directory」を選択します。

すると、settings.json ファイルに以下の内容が追記されていると思います。

.vscode/settings.json

{
    "python.testing.pytestArgs": [
        "."
    ],
    "python.testing.unittestEnabled": false,
    "python.testing.pytestEnabled": true
}

テストファイルの作成

次に、collatz.py のテストファイル test_collatz.py を作成します。テストは後で書くことにして、テスト用の関数だけ用意しておきます。 それぞれ is_evencollatz 関数のテスト用関数です。

test_collatz.py

def test_is_even():
    pass

def test_collatz():
    pass

設定がうまくいっていれば、テストエクスプローラーにテスト一覧が表示されます。

実行

まずは test_is_even 関数のテストから実装してみます。

is_even 関数は、引数 n が偶数であれば True を、奇数であれば False を返します。よって、n が偶数のとき True を、奇数のとき False を返すことをそれぞれテストすればよいことになります。

pytest の pytest.mark.parametrize を使用することで複数パターンのテストを一つのテスト関数でできるので、これを活用して偶数の場合と奇数の場合のテストを test_is_even 関数で行います。

まず、test_collatz.py ファイルの先頭に必要な import 文を追加した上で、test_is_even 関数を以下のように編集してください。

import pytest
from collatz import collatz, is_even

@pytest.mark.parametrize("n,expected", [(1, False), (2, True)])
def test_is_even(n, expected):
    assert is_even(n) == expected

簡単に解説すると、第一引数にはテスト関数に渡すパラメータ名をカンマ区切りで指定できます。ここでは nexpected としています。これが test_is_even 関数の引数の nexpected に対応しているわけです。 第二引数では、パラメータに渡す値のペアのリストを渡しています。ここでは、n=1,expected=Falsen=2,expected=True がテスト関数に渡される値になります。つまり、テストでは is_even(1)Falseis_even(2)True であることを確かめています。

それでは実行してみましょう。 関数のテストは、ガターにある緑の実行アイコンをクリックすることで実行できます。

テストが実行され、成功すると緑色のチェックが付きます。

テストが失敗した場合は、赤色のバツアイコンが表示されます。

test_collatz の方も実装しましょう。こちらは collatz 関数の戻り値が 2 つあることに注意してください。

@pytest.mark.parametrize(
    "n,expected_len,expected_max_val", [(13, 10, 40), (28, 19, 52)]
)
def test_collatz(n, expected_len, expected_max_val):
    len_, max_val = collatz(n)
    assert len_ == expected_len
    assert max_val == expected_max_val

デバッグ

テストのデバッグができるので、簡単に紹介します。

以下の画像のように、test_collatz 関数内の 1 行目にブレークポイントを打ち、テストエクスプローラーで当該関数のデバッグアイコンをクリックします。

ブレークポイントで停止します。

デバッグの基本的な使い方については、よければ以下の記事を参照してください。

blog.logical.co.jp

カバレッジ

VS Codeカバレッジを表示することができます。今回必要なのは、pytest でカバレッジを取得するプラグイン pytest-covカバレッジを表示する VS Code拡張機能 Coverage Gutters の二つです。

インストール

pytest-covpip install pytest-cov でインストールできます。Coverage Gutters については省略します。

設定

settings.jsonpython.testing.pytestArgs の中身を以下のように編集してください。

.vscode/settings.json

"python.testing.pytestArgs": [
    "--cov=.",
    "--cov-report",
    "xml"
],

また、好みではありますが、同じく settings.json に以下の設定を追記します。

"coverage-gutters.showGutterCoverage": false,
"coverage-gutters.showLineCoverage": true

この設定では、カバレッジのガター(ブレークポイントを打つ場所)での表示を無効化し、ファイル行でのカバレッジの表示を有効化しています。 前者を非表示にしているのは、ガタ―でカバレッジが表示されている箇所にはブレークポイントが打てなくなるためです*3

カバレッジの取得対象から除外する

上記の設定だと、テストファイルに対してもカバレッジが取得されます。テストファイルをカバレッジの取得対象から外すためには .coveragerc ファイルをワークスペース直下に作成し、以下のように記述します。

.coveragerc

[run]
omit = test_*.py
補足:デバッガとの競合

pytest-cov を使用すると、テストのデバッグ時にブレークポイントで停止しないという問題が発生します。

Note If you have the pytest-cov coverage module installed, VS Code doesn't stop at breakpoints while debugging because pytest-cov is using the same technique to access the source code being run. To prevent this behavior, include --no-cov in pytestArgs when debugging tests, for example by adding "env": {"PYTEST_ADDOPTS": "--no-cov"} to your debug configuration. (See Debug Tests above about how to set up that launch configuration.) (For more information, see Debuggers and PyCharm in the pytest-cov documentation.) doch

引用:https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings

上の引用文を参考に、デバッグの設定に次を追記することで対処できます。

"purpose": ["debug-test"],
"env": {"PYTEST_ADDOPTS": "--no-cov"}

.vscode/launch.json

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: 現在のファイル",
            "type": "python",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal",
            "justMyCode": true,
            "purpose": ["debug-test"],
            "env": {"PYTEST_ADDOPTS": "--no-cov"}
        }
    ]
}

表示

では実際に表示してみましょう。 テストエクスプローラーのテストファイル横の実行アイコンをクリックします。これにより、テストファイル内の全てのテスト関数が実行されます。

テスト実行後、collatz.py を開いた状態でステータスバーの「〇 Watch」をクリックします。

すると、以下のようにコード上にカバレッジが表示されます。 緑のラインはコードが実行されたことを、赤いラインはまだコードが実行されていないことを表しています。 また、先程クリックしたステータスバーの箇所にカバレッジが表示されます。ここでは 64% です。

上の画像から、collatz 関数に 0 以下の数を与えた時に例外が発生するケースがカバーされていないことが分かります。

最後にこのケースのテストを作成し、実行してみましょう。 以下のように import 文を修正し、test_collatz_not_positive_number_error 関数を test_collatz.py に追加してください。 テストはこのファイルの関数全てに対して実行することに注意してください。

test_collatz.py

from collatz import collatz, is_even, NotPositiveNumberError


def test_collatz_not_positive_number_error():
    with pytest.raises(NotPositiveNumberError) as e:
        collatz(0)

実行すると先程の赤いラインが緑色になり、カバレッジが 68% に増えていることが確認できます。

おわりに

この記事では VS Code と pytest を使用した基本的なテスト方法とデバッグ、そしてカバレッジの表示方法を扱いました。特にカバレッジを表示すると、まだカバーされていない箇所が一目瞭然になるので、何に対するテストを書いたらいいかが分かりやすくなると思います。

参考

*1:仮想環境を使用する場合は、有効化してから実行してください。

*2:そして、 最終的にどんな数であっても 1 になるであろうというのが Collatz 予想です。

*3:カバレッジの表示/非表示は簡単に切り替えられるので、そこまで手間ではないですが。。