👏

pytest書いてみた。(テストの必要性と様々な書き方)

2023/04/01に公開

必要性について

pytestとは:

pytestとは、Pythonでよく用いられる単体テスト(ユニットテスト)を支援するためのモジュール。テストの作成と実施を効率的に行うことができます。
開発者が作成した関数やメソッドが意図した通りに機能するかを確かめる単体テストを作成し、まとめて実行して結果を報告してくれます。

そもそもなぜテストが必要なのか。

  • メリット

1、バグを早期に発見し、修正することができます。
単体テストは、開発者が関数やメソッドを単独でテストすることができるので、問題が発生した場合にバグを特定しやすくなります。このようにして、バグを早期に発見して修正することができます。

2、コードの品質を確保することができます。
単体テストは、関数やメソッドが予期せぬ入力に対してどのように応答するかをテストするため、コードの品質を確保することができます。

3、リファクタリングを行いやすくなります。
単体テストを実装することで、コードを修正する際に、その修正が関数やメソッドの機能に影響を与えていないかどうかを簡単に確認できます。

  • デメリット

1、手間と時間がかかります。
単体テストを作成するためには、手間と時間がかかる場合があり、テストを実行するための環境の構築、テスト用データの作成、テストコードの作成、実行、結果の解析などが必要です。

2、コードの変更に合わせてテストコードを更新する必要があります。
コードを変更した場合、それに合わせてテストコードも更新する必要があります。このため、テストコードが多い場合は、テストコードのメンテナンスコストが高くなる場合があります。

単体テストにはいくつかのデメリットがありますが、一般的にはメリットの方が大きく、コードの品質を向上させるためには必要ということ。

書き方について

基本的な関数テスト方法

まず素数判定を行う関数を書きます。

def is_prime(n: int) -> bool:
    if n <= 1:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False
    i = 3
    while i * i <= n:
        if n % i == 0:
            return False
        i += 2
    return True

次にこの関数に対するテストを下記のように記述します。

from prime import is_prime

def test_is_prime():
    assert not is_prime(1)
    assert is_prime(2)
    assert is_prime(3)
    assert not is_prime(4)
    assert is_prime(5)
    assert not is_prime(6)
    assert is_prime(7)
    assert not is_prime(8)
    assert not is_prime(9)
    assert not is_prime(10)

pytest は test_ で始まるファイル・関数を単体テストのコードとみなします。テストしたい関数を import 文で取り込み、assert という文の後ろにテストしたい式を記述します。

テストの実行

テストを実行するには pytest というコマンドを使います。

$ pipenv shell
(prime) $ pytest test_prime.py
============================================== test session starts ==============================================
platform darwin -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.11.0
rootdir: /Users/kenichiro-ida/Documents/github.com/rinatz/prime
collected 1 item

test_prime.py .                                                                                           [100%]

=========================================== 1 passed in 0.02 seconds ==============================

簡単に説明してしまいましたが、基本的にはテストしたい関数をimportしてきて、assert でその関数の期待値を記述します。

ファイルを扱う関数のテスト方法

ファイルを扱う関数はfixture(フィクスチャ)が有効です。
そもそもファイルを扱う関数とは?
ファイルを扱う関数は、主にファイルを読み書きするための機能を持ちます。
例として2つあげます。
1つはファイルを開いて、ファイルオブジェクトを取得する関数です。

f = open('example.txt', 'r') # ファイルを読み込み専用で開く
content = f.read() # ファイルの内容を読み込む
f.close() # ファイルを閉じる

2つ目は、ファイルオブジェクトにデータを書き込む関数です。

f = open('example.txt', 'w')
f.write('Hello, world!') # ファイルに文字列を書き込む
f.close()

他にもありますが、これらが、ファイルを扱う関数と思ってもろて良いです。

フィクスチャは、テストコードで使用するテストデータやテスト環境を事前に準備するためのものです。以下に、pytestでフィクスチャを定義する構文と使用例を示します。

import pytest

@pytest.fixture
def my_fixture():
    # テストデータのセットアップ
    data = [1, 2, 3]
    yield data  # テストデータを返す
    # テストデータの後処理

上記の例では、@pytest.fixtureデコレータを使用して、my_fixtureという名前のフィクスチャを定義しています。yield文でテストデータを返すことができます。また、フィクスチャの後処理が必要な場合は、yield文の後に後処理のコードを記述することができます。

フィクスチャをテストコードで使用するには、pytestの自動引数解決機能を使用します。以下に、my_fixtureを使用したテストコードの例を示します。

def test_my_fixture(my_fixture):
    assert len(my_fixture) == 3
    assert 1 in my_fixture
    assert 4 not in my_fixture

上記の例では、test_my_fixture関数でmy_fixtureを引数として受け取っています。my_fixtureは、事前に定義されたフィクスチャから自動的に解決されます。test_my_fixture関数では、my_fixtureを使用してテストを実行しています。

これらの例から、フィクスチャを定義することで、テストデータのセットアップや後処理を簡単に行うことができることが分かります。フィクスチャを適切に使用することで、テストコードの保守性や再利用性を高めることができます。

モック化する方法

モックとは、テストで使用するダミーオブジェクトのことです。モックオブジェクトは、実際のオブジェクトと同じように動作しますが、テスト用に設定された値を返します。これにより、テストの実行速度を向上させることができます。また、モック化させることで実際のAPIを使わずに、安全にテストを行うこともできます。もし、モック化を使わないでテストを行った場合、本番用のAPIを動かしてしまい、悪影響を及ぼしてしまう場合もあります。

Pytestでは、unittest.mockモジュールを使用して、モックオブジェクトを作成できます。以下に、モックを使用してPytestを行う手順を説明します。

1、テスト対象のコードを作成します。
2、テスト対象のコードに依存するオブジェクトをモック化します。
3、テストコードを書きます。
4、モックオブジェクトを使って、テストコードを実行します。
5、テスト結果を検証します。

例:

# 対象のコード
def get_data():
    # 何らかのデータを取得する処理
    pass

def process_data():
    data = get_data()
    # データを処理する処理
    pass

# テストコード
from unittest.mock import patch

@patch('get_data')
def test_process_data(mock_get_data):
    mock_get_data.return_value = [1, 2, 3]  # モックの戻り値を設定
    process_data()  # モックオブジェクトを使用して処理を実行
    mock_get_data.assert_called_once()  # モックオブジェクトが呼び出されたことを確認

この例では、get_data関数をモック化し、process_data関数をテストしています。@patchデコレータを使用して、get_data関数をモック化しています。mock_get_data.return_valueを使用して、モックオブジェクトの戻り値を設定しています。process_data関数を呼び出す際に、モックオブジェクトを使用してデータを取得するように設定されています。最後に、mock_get_data.assert_called_once()を使用して、モックオブジェクトが1回呼び出されたことを確認しています。

このように、モックオブジェクトを使用して、依存関係のあるオブジェクトを置き換えてテストを行うことができます。

以上、pytestでした!

Discussion