ひとりでのアプリ開発 - fineの備忘録 -

ひとりでアプリ開発をするなかで起こったことや学んだことを書き溜めていきます

Python - pytest:pytestを用いてテストをする

初めに

 Pythonでテストを作成するとき、pytestと呼ばれるモジュールを使用します。本記事はpytestの使い方を簡単にまとめます。

pytest

pytestとは

 pytestとは、python単体テストを書くためのフレームワークの1つです。

単体テストを書くためのフレームワーク

フレームワーク 説明
unittest 標準ライブラリ
nose かつて主流だったもの
pytest 現在主流のもの

 他のフレームワークを使って記述することもありますが、現在はpytestを使って書かれることが多いようです。

単体テストとは

 開発をするときに行われるテストには、次のような種類があります。

種類 説明
単体テスト 機能・操作画面ごとに行う。関数単体
結合テスト 他の機能と連動させたときに行う。関数を複数結合したとき
システムテスト システム全体の動作テスト

 

使い方

ソースコードが1つの場合

 テストしたいソースコードが1つの場合から始めます。

 テストしたいソースコードと同じフォルダに、テスト用の .py ファイルを作成します。


  • code1:テストしたいソースコード
  • test_code1:テスト用ソースコード
  • __pycache__:テスト後自動作成される
  • .pytest_cache:テスト後自動作成される

 今回は簡単な足し算をするための関数をテストします。

# code1.py

def sum_num(x, y):
    return x + y
import code1

# code1.py の sum_cum() テスト用関数
def test_sum_num():
    result = code1.sum_num(2, 3)
    assert result == 5    # 正しい結果

 テストを実行するには、TERMINALでpytest ファイル名というコマンドを使います。

(今回のコマンド)

pytest test_code.py

 テストの結果が表示されます。今回は1つのテストが合格していることが分かります。

 仮にassert result == 3と書き換えてテストを実行してみると次のようになります。FAILUREと表示されている部分にエラーが起こった関数が表示されます。FAILEDの部分に具体的に誤りがあった部分が表示されます。

 テストをする際は、あらゆる場合を想定しどのような場合でも変な挙動をしないか確認する必要があります。そのため、次のように場合を増やし、どのような場合でも正しい処理がされるか確認します。

import code1

def test_sum_num():
    assert code1.sum_num(1, 2) == 3
    assert code1.sum_num(0, 4) == 4
    assert code1.sum_num(2, -2) == 0
    assert code1.sum_num(-5, 3) == -2
    assert code1.sum_num(23, 77) == 100
    assert code1.sum_num(-10, -4) == -14

※実際には、上のテストは不十分で float 型の場合などが考慮できていません。

パラメータ化したテスト

 上記のように毎回、assertなどと記述するのは冗長であるため、パラメータ化することを考えます。

 テストで使用するパラメータは@pytest.mark.parametrize()デコレータを用い、記述します。

 試しに、次の引数で与えられた数字が素数かどうか判定する関数をパラメータ化したテストで検証してみます。

# code1.py

def is_prime(n):
  """
  引数が素数かどうかを判定する関数。

  Args:
      n: 判定対象の数

  Returns:
      n が素数であれば True、そうでなければ False
  """
  if n <= 1:
    return False
  for i in range(2, int(n**0.5) + 1):
    if n % i == 0:
      return False
  return True

 次がテスト用コードです。

# code_test.py

import pytest
import code1

# テスト用パラメータ
@pytest.mark.parametrize(("num", "expected"), [
    (1, False),
    (2, True),
    (3, True),
    (4, False),
    (5, True),
    (6, False),
    (7, True),
    (8, False),
    (9, False),
    (10, False)
])

def test_sum_num(num, expected):
    assert code1.is_prime(num) == expected

 @pytest.mark.parametrize()デコレータを使うために、pytestをインポートしています。テスト用のパラメータを作り、そのパラメータの値をテストに使っています。@pytest.mark.parametrize()で指定する引数は、テスト用関数の引数になります。

ソースコードが1つでない場合

 ソースコードが1つでない場合は、プロジェクト構成をきちんとしなければテストが正しく実行できなくなります。

 注意点は次の2つです。

  • テストを複数ファイルに分割して書く場合はtestsディレクトリを作成し、その中にテスト用ソースコードを作成する。
  • testsディレクトリに__init.py__を作成する。__init.py__は白紙でよい。
モック

 モックは、テスト対象のコードで実際に使用されるオブジェクトの変わりに使用する偽にオブジェクトです。

 モックは、以下のような場合に使用されます。

  • テスト対象のコードが依存している外部ライブラリやサービスがテスト時に利用できない場合
  • テスト対象のコードの特定の動作のみを検証したい場合
  • テスト対象のコードの挙動を制御して、様々なケースを想定したテストを書きたい場合

(テストしたい関数)

def get_name():
  """ユーザーから名前を入力し、返す関数です。

  Returns:
      str: 入力された名前。
  """
  name = input("名前を入力してください: ")
  return name

def get_name_length():
  name = get_name()
  return len(name)

 今回は入力された名前を返す関数を呼び出し、その名前の文字数を返す関数get_name_length()をテストします。これはテスト段階では、get_name()が実行できないため、モックを使い、テストをする必要があります。

 モックを使うには、pytest-mock モジュールをインストールする必要があります。TERMINALで次のコマンドを実行しインストールします。

pip install pytest pytest-mock

 モックオブジェクトを生成するにはmocker.patch()を使います。

import pytest
import code1

def test_get_name_length(mocker):
    # モックオブジェクトを生成
    # モックにする関数を引数にする
    mock_get_name = mocker.patch("code1.get_name")

    # モックオブジェクトの設定
    # 返り値を指定
    mock_get_name.return_value = "山本源流斎重国"

    # テスト対象の関数を実行
    result = code1.get_name_length()

    # 検証
    assert result == 7

 モックオブジェクトの設定は次のように返り値の設定も合わせた形で記述することもできます。

mocker.patch("code1.get_name", return_value = "山本源流斎重国")

 モックオブジェクトが呼ばれた回数を検証することが可能です。下記のようなコードをテスト用関数の最下部に書くことで検証できます。

# テスト用関数の最下部に記述
# m はモックオブジェクト
    assert m.call_count == 3 # call_countで呼ばれた回数を取得
    m.assert_called()        # 少なくても1度は呼ばれた事の検証
    m.assert_called_once()   # 1回だけ呼ばれた事の検証
    m.assert_not_called()    # 1回も呼ばれていない事の検証