pytest-benchmarkでboto3&motoを含めたClassBasedなコードを計測する方法について試行錯誤してみた

はじめに

Pythonで定期的にベンチマークを図りたい場合にpytest-benchmarkという選択肢があります。既存のテストに混在させた場合でも、実行時のオプション指定で動作切り分けも可能です。

ただ、pytest-benchmarkをboto3 + moto + ClassBased-Testで利用したケースについては、調べた限りではたどり着くことができませんでした。以下の記事を参考にしつつ、試行錯誤した結果をまとめてみました。

Python: pytest-benchmark でベンチマークテストを書く - CUBE SUGAR CONTAINER

pytest-benchmarkの導入から実行用指定まで

とくに変わった指定はありません。念の為benchmarkだけを動かす前提とします。

導入

pipenv install --dev pytest-benchmark

実行用指定

pytest -v --benchmark-only

boto3 + motoと併用

簡単なサンプルコードを用意してみました。

import pytest
import boto3
from moto import mock_s3

class TestDemoClass:
    
    def setup_class(cls):
        pass
        
    def teardown_class(cls):
        pass
        
    def setup_method(self, name):
        pass
        
    def teardown_method(self, name):
        pass
        
    @mock_s3
    def process_main(self, duration=0.000001):
        # any process
        client = boto3.client('s3')
        ..
        return True
        
    def test_benchmark(self, benchmark):
        ret = benchmark.pedantic(self.process_main, rounds=100, iterations=100)
        assert ret is True
% pytest -v --benchmark-only

================= test session starts =========================
platform darwin -- Python 3.7.3, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 -- /XXXXX/.venv/bin/python3.7
cachedir: .pytest_cache
benchmark: 3.2.2 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /XXXXX, inifile: setup.cfg, testpaths: tests/
plugins: benchmark-3.2.2, cov-2.7.1
collected 1 items

test_demo.py::TestDemoClass::test_benchmark PASSED

----------------- benchmark: 1 tests -------------------------
Name (time in ms)         Min      Max     Mean  StdDev   Median     IQR  Outliers      OPS  Rounds  Iterations
--------------------------------------------------------------
test_benchmark        17.7854  30.1732  19.3668  1.2481  19.3553  0.7839       6;1  51.6349     100         100
--------------------------------------------------------------
================= 1 passed in 196.20 seconds ============================

pytest-benchmarkを次の順番で実装・実行しています。

  1. benchmarkを図る処理を実装したメソッドprocess_main(非テスト)を用意する
  2. 1で実装したメソッドを呼ぶbenchmark実行用testメソッドtest_benchmarkを用意する
  3. 2で実装したメソッドを実行する

また、試行回数と呼び出し回数も設定する場合にどうなるかも記載するため、pedanticでの呼び出しにしています。process_mainでTrueを返しているのは、benchmark完了をもってassertを実行するためです。

process_mainの中で非test実施用メソッドを実行したい場合は、self.XXX()の呼び出しで問題ありません。

実行時

boto3による想定外なAWSへのアクセスが発生してしまわないようにするため、本番環境以外ではaws profile用変数にサンプル的な文字列を設定することをおすすめします。

また、実行時間が超過することも自明であるため、CircleCIの設定ファイルには忘れずに--benchmark-skipを指定しておきましょう。

pipenv run pytest --benchmark-skip

気をつけるべき点

conftest.py等からの@pytest.fixture呼び出し、及び@pytest.mark.parametrizeでのfixture分離実装の併用が効きませんでした。

まとめ

benchmark用処理は特に扱いに関して慣例的なものがなく、コードがまちまちなど管理し辛い点も発生しやすい代物です。ですが、pytest-benchmarkによるtestコードベースでの管理であれば、実行手段の絞り込み等も含めて共通認識は維持しやすいでしょう

boto3の処理についてはAWS側のレスポンスに大きく左右されますが、それ以外のコードでのベンチマークを計って置きたい場合には有効な手段だと思います。