Pandas, OpenPyXL で pytest の手習い

2022.09.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

pytestの使い方を学びます。テストケースには、個人的に直近で利用していたPandas, OpenPyXLを利用したいと思います。 手順は、pytest を参考にさせていただきます。

環境/前提

こちらの記事と同様の環境を利用します。

【Tips集】 Pandas,OpenPyXL を利用したエクセル読み書き

なお、今回は追加で以下のパッケージが必要です。
- numpy 1.23.3
- pytest 7.1.3

テストの書き方/テスト実行(アサーションチェック)

まずは、単純なテストケースを利用し、pytestの書き方を学びます。

pytest は test_ で始まるファイル・関数を単体テストのコードとみなします
テストの書き方

pandas Testingのassert_series_equal関数は、デフォルトでdtypeまで確認してくれます(第3引数: check_dtype=True1)。

テストケースは、Book1.xlsxファイルの2行目(A:E)に入っているデータが期待したものになっているかのチェックです。

import pytest
import numpy as np
import pandas as pd
from pandas.testing import assert_series_equal
import openpyxl

def test_read_excel():
    actual = pd.read_excel('Book1.xlsx', sheet_name='Sheet1', usecols="A:E", dtype = {
        'メールアドレス1(必須)': 'string',
        'メールアドレス2(任意)': 'string',
        'メールアドレス3(任意)': 'string'
    })
    assert_series_equal(actual['ID'], pd.Series([12345], dtype=np.int64, name='ID'))
    assert_series_equal(actual['日付'], pd.Series([pd.to_datetime('2022/08/31')], dtype='datetime64[ns]', name='日付'))
    assert_series_equal(actual['メールアドレス1(必須)'], pd.Series(['shiraishi@example.com'], dtype='string', name='メールアドレス1(必須)'))
    assert_series_equal(actual['メールアドレス2(任意)'], pd.Series(['shiraishi@example.com'], dtype='string', name='メールアドレス2(任意)'))
    assert_series_equal(actual['メールアドレス3(任意)'], pd.Series(['shiraishi@example.com'], dtype='string', name='メールアドレス3(任意)'))
$ pytest test_xl_test.py
======================================================================================== test session starts =========================================================================================
platform darwin -- Python 3.10.6, pytest-7.1.3, pluggy-1.0.0
rootdir: /Desktop/test_python
collected 1 item                                                                                                                                                                                     

test_xl_test.py .                                                                                                                                                                              [100%]

========================================================================================= 1 passed in 0.61s ==========================================================================================

パラメータ化したテスト

次に@pytest.mark.parametrizeデコレータを使います。
デコレータは、テストで使用する値をパラメータとして持つことができます。初めの引数(expected)は、テスト関数に渡すパラメータの引数名を指定することができます。

テストケースは、Book1.xlsxファイルの2行目1列(A2)に入っている'ID'が期待したものになっているかのチェックです。

import pytest
import numpy as np
import pandas as pd
from pandas.testing import assert_series_equal
import openpyxl

@pytest.mark.parametrize(('expected'), [
    (pd.Series([12345], dtype=np.int64, name='ID')),
])
def test_read_excel_2(expected):
    actual = pd.read_excel('Book1.xlsx', sheet_name='Sheet1', usecols="A:E", dtype = {
        'メールアドレス1(必須)': 'string',
        'メールアドレス2(任意)': 'string',
        'メールアドレス3(任意)': 'string'
    })
    assert_series_equal(actual['ID'], expected)
$ pytest test_xl_test.py
======================================================================================== test session starts =========================================================================================
platform darwin -- Python 3.10.6, pytest-7.1.3, pluggy-1.0.0
rootdir: /Desktop/test_python
collected 2 items                                                                                                                                                                                    

test_xl_test.py ..                                                                                                                                                                             [100%]

========================================================================================= 2 passed in 0.63s ==========================================================================================

フィクスチャ

最後にフィクスチャを利用します。
フィクスチャでは、テスト関数を実行する前の前処理を記述することができます。今回は、前処理としてBook1.xlsxの2行目(A:E)のデータを書き換えます。

テストケースは、Book1.xlsxファイルの2行目1列(A2)に入っている'ID'が期待したもの(書き変わった値)になっているかのチェックです。

import pytest
import logging
import numpy as np
import pandas as pd
from pandas.testing import assert_series_equal
import openpyxl

@pytest.fixture
def xl_name() -> str:
    wb = openpyxl.load_workbook('Book1.xlsx')
    ws = wb["Sheet1"]
    
    for i, values in enumerate([['55555', '2022/09/30', 'shiraishi@example.com', 'shiraishi@example.com', 'shiraishi@example.com']]):
        for j, value in enumerate(values):
            if value is None:
                continue
            ws.cell(row=2+i, column=1+j, value=value)
    wb.save('Book1.xlsx')

    yield 'Book2.xlsx'

def test_read_excel_3(xl_name):
    actual = pd.read_excel(xl_name, sheet_name='Sheet1', usecols="A:E", dtype = {
        'メールアドレス1(必須)': 'string',
        'メールアドレス2(任意)': 'string',
        'メールアドレス3(任意)': 'string'
    })
    assert_series_equal(actual['ID'], pd.Series([55555], dtype=np.int64, name='ID'))
$ pytest test_xl_test.py
======================================================================================== test session starts =========================================================================================
platform darwin -- Python 3.10.6, pytest-7.1.3, pluggy-1.0.0
rootdir: /Desktop/test_python
collected 3 items                                                                                                                                                                                    

test_xl_test.py ...                                                                                                                                                                            [100%]

========================================================================================= 3 passed in 0.64s ==========================================================================================

所感

昔、xSpec系のテストを学ぶ機会がありました2。xUnit系のテストをあまり書いた経験がなかったのですが、pytestは、このような良い指南書もあり、入門には良いテストライブラリだと感じました。次のステップとしては、どのように考え、どのようなテストを行うか、やテストダブルのおさらいをしたいと思います。以上、pytest入門でした。

参考