【pytest-bdd】featureファイルでデータテーブルを定義したい

データテーブルを定義できないらしい

Cucumberではgivenで、テーブル構造で引数を定義することができる。
Cucumber Data Tables

Feature: データテーブルサンプル
    Scenario: データテーブルを使う
        Given XXデータが設定されている
            | 名前 | 年齢 |
            | 山田 | 22   |
            | 佐藤 | 23   |
            | 田中 | 33   |

しかし、pytest-bddではこの記法に対応していない。

(pull requestは2016年に作製されているようだが、まだ対応していない)
Add support for data tables (Github pytest-dev/pytest-bdd)

Parserを拡張して対応

テーブル構造でデータを定義できないと不便なため、なんとかしたい。

ドキュメントにカスタムparserの記述があったため、
Step arguments (Github pytest-dev/pytest-bdd)

標準のparserを拡張してテーブル部分のparse処理を追加する方式で対応してみることにした。

拡張parser

標準で用意されているparsers.parseを拡張して実装した。

from pytest_bdd import parsers


class TableDataParser(parsers.parse):
    """テーブルデータ用parser"""

    def __init__(self, name: str, data_table_key: str, columns_name_alias={}, **kwargs):
        """Parser初期化処理
        Args:
            name ([type]): parse定義
            data_table_key (str): データテーブル部を割り当てる変数名
            columns_def (dict): 列名の別名定義
        """
        # テーブル部を定義に追加して初期化する
        super().__init__(f"{name}\n{{{data_table_key}}}", **kwargs)

        # データテーブルの変数名
        self.data_table_key = data_table_key

        # 列別名定義
        self.columns_name_alias = columns_name_alias


    def parse_arguments(self, name):
        """引数をパースする
        :return: `dict` of step arguments
        """
        # 標準のparse処理
        arguments = super().parse_arguments(name)

        # テーブル部をparseする
        data_table_rows = arguments[self.data_table_key].split("\n")
        data_table = [
            self._parse_table_row(row_str) for row_str in data_table_rows
        ]

        # 1行目は列定義
        col_def = data_table.pop(0)
        # 列名に別名定義があれば差し替える
        col_def = [
            col if col not in self.columns_name_alias else self.columns_name_alias[col]
            for col in col_def
        ]

        # 辞書の配列に組み替える
        data_table_dicts = [
            {
                col: row[i]
                for i, col in enumerate(col_def)
            }
            for row in data_table
        ]

        # データテーブルをparse後の値に差し替える
        arguments[self.data_table_key] = data_table_dicts

        return arguments


    def _parse_table_row(self, row_str: str) -> list[str]:
        """データテーブルの行をparseする
        Args:
            row_str (str): データテーブル 1行分の文字列
        Returns:
            list[str]: 行を列単位に分割した配列
        """
        # 列単位に分割
        # 先頭と末尾は不要
        cols = row_str.split("|")[1:-1]

        # 列ごとに空白を取り除く
        cols = [col.strip() for col in cols]

        return cols

使用部分

    Scenario: データテーブルを使う
        Given XXデータが設定されている
            | 名前 | 年齢 |
            | 山田 | 22   |
            | 佐藤 | 23   |
            | 田中 | 33   |
@given(TableDataParser("XXデータが設定されている", "xx_data_table"))
def settted_XX(xx_data_table):
    print(xx_data_table)
    # [
    #     {'名前': '山田', '年齢': '22'},
    #     {'名前': '佐藤', '年齢': '23'},
    #     {'名前': '田中', '年齢': '33'}
    # ]

標準のparserを拡張しているので標準の変数記法も利用できる。

@when(TableDataParser("{target}を設定する", "target_data_table")
def set_data(target, target_data_table):
    print(target)
    print(target_data_table)

扱いやすいように、列別名を定義できるようにしてみた。

@when(TableDataParser("XXデータが設定されている", "target_data_table", columns_name_alias={
    "名前": "name",
    "年齢": "age",
}))
def settted_XX(target_data_table):
    print(target_data_table)
    # [
    #     {'name': '山田', 'age': '22'},
    #     {'name': '佐藤', 'age': '23'},
    #     {'name': '田中', 'age': '33'}
    # ]

いまいちなところ

今の作りだと各列のデータ型を指定できないため、string以外で扱いたいときに面倒。

parserの引数で型を定義できるようにしたほうがいいかも。