pytest を爆速にする10の方法
十分に速いと思っていても、10倍の速さでテストが終われば使い方が変わります。
私の事例では、171秒 が 30秒 に、82%削減しました。17秒(90%削減)を目指したい。
テストが爆速になると、色々なメリットがあります。
分かりやすいところでは、手元で実行するのに「待たなくて良い」ので、気軽に再実行できます。
また、CI の実行待ち行列が解消し、サービス利用料の削減にも繋がります。 10の方法
1. pytest-xdist を使って、並列実行する
2. DBにはsqlite3のメモリDBをできるだけ使う
3. DBトランザクションテストをやめる
4. DBのレコードを作らない
5. テスト用のデータ量を絞る
6. 外部通信しない
7. coverage では sysmon を使う
8. テスト起動時の不要なインポートを止める
9. pytestの探索パスをしぼる
10. pytest-profiling でテスト実行のボトルネックを探る
おまけ
GitHub Actions のJobを並列化する
uv を使ってCI上でのパッケージインストールを高速化する
また、テスト高速化のために、テストの質を落とさないことも重要です。
テストが爆速になれば、むしろこれまでカバーできていなかった部分までテストできるようになります。
--------
以下、実データを収集
0. 前提、調べ方
pytest --durations 10
テストケース実行時間を遅い順に把握する
pytest --profile-svg
全体的になにがボトルネックになっているか把握する
1. pytest-xdist を使って、並列実行する
refs: PR#5972
実行時間を 53% 削減しました
table:xdist並列化の効果
導入前(直列) 171s
導入後(並列) 80s
並列実行でコンソール出力が壊れてしまったため、導入しました。
2. DBにはsqlite3のメモリDBをできるだけ使う
改善
修正前: TBD
修正後: TBD
UnitTestでDBを扱うと遅い
そのテスト、本当にMySQLを使う必要ある?
3. DBトランザクションテストをやめる
refs: PR#5972 test_models.py#L46
改善
修正前: TBD
修正後: TBD
前提
モデルの削除signalで transaction.on_commit を呼び出している処理がある
signals処理はテストの範疇だけど、on_commitから先はテスト対象ではない
テストを通すにはトランザクションを有効化して実行しないとエラーになってしまうので、有効化している
修正前
code:python
@pytest.mark.django_db(transaction=True)
def test_delete_object(...):
# Arrange
obj = Model.objects.create(...)
# Act
obj.delete()
# ここで、signal経由で transaction.on_commit() しているため、transaction=Trueが必要だった
修正後
code:python
def test_delete_object(...):
# Arrange
obj = Model.objects.create(...)
# Act
with patch("django.db.transaction.on_commit", side_effect=lambda func: func()):
obj.delete()
4. DBのレコードを作らない
refs: PR#6004 test_models.py#L46
前提
parametrizeテストで207ケースを実行
DBモデルインスタンスの値が参照出来ればテストができる
改善
修正前: TBD
修正後: 1秒ちょっと
修正前
code:python
table = TableFactory() # DB登録されたModelインスタンスを返す
修正後
code:python
table = TableFactory.stub(id=1) # save前のModelインスタンスを返す
でもFKデータを table.columns.add(col) できないよ?
我々にはmockがあるじゃない!
code:python
columns = []
def add(col):
columns.append(col)
table = TableFactory.stub(id=1)
table.columns = MagicMock()
table.columns.add.side_effect = add
5. テスト用のデータ量を絞る
refs: PR#5972 src/page/tests/test_view.py#L503
前提
1ページ50件でページング設定(システム全体)
ページングの動作確認のためにダミーデータを100件作成
改善
修正前: TBD
修正後: TBD
修正前
code:python
# Arrange
from core.paginations import PageNumberPagination
# Assert
assert len(res.json_body"results") == PageNumberPagination.page_size 修正前
code:python
# Arrange
from core.paginations import PageNumberPagination
monkeypatch.setattr(PageNumberPagination, "page_size", 2) # 1ページ2件に上書き
# Assert
6. 外部通信しない
7. coverage では sysmon を使う
8. テスト起動時の不要なインポートを止める
9. pytestの探索パスをしぼる
10. pytest-profiling でテスト実行のボトルネックを探る