Zeals TECH BLOG

チャットボットでネットにおもてなし革命を起こす、チャットコマース『Zeals』を開発する株式会社Zealsの技術やエンジニア文化について発信します。

pytestのparametrizeの使い方とその有用性について

f:id:RyotaAraki:20191211175222j:plain

※こちらはZeals AdventCalendar 12日目の記事です。

こんにちは!

Pythonエンジニアの荒木です。
夏頃に社内の開発プロジェクトにPythonメンバーとしてアサインされ、本格的にPythonを業務で書くようになり、最近はほぼ毎日Pythonを書いています。

Pythonの業務としては主に新機能実装とDBリファクタリングをやっています。

今日はpytestのparametrizeの基本的な使い方
parametrizeでちょっと詰まったところがあったので、それらについて書きます。

目次は以下のようになります。

前提

まず、以前にこちらの記事で紹介したように、Zealsは、LINEおよびFacebook Messenger上で動作するチャットボットサービスで、サービスは大きく分けると、

  1. チャットボットの管理用Webアプリケーション
  2. チャットボットへのメッセージ送受信サービス
  3. 1と2から共通処理を切り出したマイクロサービス群

の3つに分かれています。
詳細はこちらの記事をご覧ください。

tech.zeals.co.jp

その中の 2.メッセージ送受信機能フレームワークを採用しないピュアなPythonで実装しており、チャットボットに流入したエンドユーザーに対して、設定されたシナリオの応答や配信処理 を行います。
実際に、10万人を超えるユーザーに対する配信もここで行っています。

このようにZealsにおけるPythonコードは直接エンドユーザーとやりとりするので、誤った挙動をしてしまうとそのまま配信事故につながるリスクが大きいです。
例えば、想定していた配信先と異なる配信先にメッセージを送信して閉まった場合は、そのまま配信事故に繋がることになります。

そのためにこの配信機能の開発はより慎重さが求められ、当然テストコードの重要性も高くなります

parametrizeについて

parametrize とは

Zealsでは、Pythonのテストフレームワークとしてpytestを導入しています。

parametrizeとはpytestで使えるデコレータの1つで変数と結果をパラメータでテストコードに渡すことで、1つのテストメソッドで複数パターンのテストを行うことができます。

parametrizeを使ってテストコードを書くことで
テストコードをDRYにして改修コストを下げることができます。

parametrize の使い方

実際にparametrizeを使ってみます。

import pytest


numeric_datas = [
    (2, 5, 10),
    (3, 6, 18),
    (5, 9, 45)
]


@pytest.mark.parametrize("a, b, expect", numeric_datas)
def test_multiple(a, b, expect):
    assert a*b == expect

与えた2つの変数a, bの積がexpectであるかどうかを判別するテストです。 これを実行してみると、

============================ test session starts =============================
platform darwin -- Python 3.7.4, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: /Users/hogehoge/Workspace/myproject
collected 3 items

test_parametrize.py ...                                                [100%]

============================= 3 passed in 0.01s ==============================

となり、いずれの計算結果も正しかったことが分かりました。
このブログのコンソールのUIは白黒ですが、実際はテストが通ると1番下のラインが綺麗な緑色になります。

試しに与えるexpectを誤ったものに変えてみます。

numeric_datas = [
    (2, 5, 8),
    (3, 6, 18),
    (5, 9, 45)
]

この内容でテストを実行すると、

============================ test session starts =============================
platform darwin -- Python 3.7.4, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: /Users/hogehoge/Workspace/myproject
collected 3 items

test_parametrize.py F..                                                [100%]

================================== FAILURES ==================================
____________________________ test_multiple[2-5-8] ____________________________

a = 2, b = 5, expect = 8

    @pytest.mark.parametrize("a, b, expect", numeric_datas)
    def test_multiple(a, b, expect):
>       assert a*b == expect
E       assert (2 * 5) == 8

test_parametrize.py:16: AssertionError
======================== 1 failed, 2 passed in 0.05s =========================

このように、どのパラメータを与えた時にテストが落ちたのかが分かります。
あるパラメータでテストが通過しなくても、他のパラメータが別で独立してテストを続行してくれるところはありがたいですね。

parametrizeについてのドキュメントはこちらです。

docs.pytest.org

実際のZealsにおけるparametrizeのユースケースとしては
LINEとFacebook Messengerで共通した処理のテストをLINE, Facebook Messengerそれぞれの変数をパラメータで渡したり、個人情報に関するメッセージの情報の種類(郵便番号や住所など)をパラメータとして渡すことで活用しています。

fixtureをparametrizeする

さて、次はfixtureのparametrizeについてです。

fixtureとは

fixtureとはpytestで使い回すことができる、テスト用のインスタンスを定義しているメソッドのことです。

実際にpytestでテストメソッドを回す際には、fixtureをテストメソッドの引数に与えてテストを回すことが多いです。
引数として与えられたfixtureはテストメソッドが実行される前に必ず実行されるようになっています。

fixtureのparametrize

やりたかったことは「このfixtureをパラメータに渡してテストメソッドを回したい」ということでした。

そのため、↓のようにfixtureを定義してメソッドを実行してみます。

@pytest.fixture
def a():
    return 'A'

@pytest.fixture
def b():
    return 'B'

@pytest.fixture
def c():
    return 'C'


characters = [
    ('a', 'A'),
    ('b', 'B'),
    ('c', 'C')
]


@pytest.mark.parametrize("letter, expect", characters)
def test_capitalize(letter, expect):
    assert letter == expect

実際に実行すると、いずれのテストも失敗します。
テストケース3つともすべて同じ原因で失敗をするのですが、エラーメッセージは以下のように表示されます。

____________________________ test_capitalize[a-A] ____________________________

letter = 'a', expect = 'A'

    @pytest.mark.parametrize("letter, expect", characters)
    def test_capitalize(letter, expect):
>       assert letter == expect
E       AssertionError: assert 'a' == 'A'
E         - a
E         + A

test_parametrize.py:47: AssertionError

letterがfixtureとして認識されていないために、変数 'a' として返されていることが分かります。

そこで、与えた変数をfixtureに変換してあげる必要があります。
その変換のためのメソッドが getfixturevalue() です。

公式ドキュメントは↓です。

docs.pytest.org

これを用いて変数を変換してみましょう。

@pytest.fixture
def a():
    return 'A'

@pytest.fixture
def b():
    return 'B'

@pytest.fixture
def c():
    return 'C'


@pytest.fixture
def get_fixture_values(request):
    def _get_fixture(fixture):
        return request.getfixturevalue(fixture)
    return _get_fixture


characters = [
    ('a', 'A'),
    ('b', 'B'),
    ('c', 'C')
]


@pytest.mark.parametrize("letter, expect", characters)
def test_capitalize(letter, expect, get_fixture_values):
    letter = get_fixture_values(letter)
    assert letter == expect

これを実行すると、

============================ test session starts =============================
platform darwin -- Python 3.7.4, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: /Users/mbp028/Workspace/myproject
collected 3 items

test_parametrize.py ...                                                [100%]

============================= 3 passed in 0.02s ==============================

テストが問題なく通過しました。
ちなみに、Zealsではこのメソッドをpluginで実装しており、Pythonアプリケーション内の様々なファイルから呼び出すことができるように工夫しています。

終わりに

Pythonは日本語の情報が少なく、ましてやpytestやparametrizeと
より細かい領域の話であれば、情報を集めるのになおさらハードルが上がってしまうと感じます。

そのような現状で、この記事が少しでも誰かの役に立てば幸いです。

また、Zealsでは一緒にビジョンを追いかける仲間を募集しています!

技術を通して明るい未来の社会を創りたい方、ぜひ下記募集よりご応募ください。

hrmos.co