pytestで並列でテストを実行しつつカバレッジを取得する方法
はじめに
現代の開発ではテストコード及びCI環境は欠かせません。しかし、開発規模が大きくなるにつれて、テストの数が増えて、だんだん実行時間が長くなってきます。そしてテストの実行時間が長くなるほど開発者へのフィードバックにかかる時間が長くなり、開発速度の低下を招きます。10分以上かかるなら危険信号と言っていいでしょう。
テスト実行速度の低下を避けるためにはデータベースや画面など他のものに依存しない、なるだけ軽量なオブジェクトを使用してテストするのが最良ですが、並列化で対応することも一つの手段です。
また、コードカバレッジも開発における重要な指標の一つです。「カバレッジが高い=品質が高い」とは言えませんが、カバレッジが高いほど安心して開発できるのは事実です。また、カバレッジを計測しておくと、テストコードが間違っていたときに気づきやすくなります[1]、そのため、コードカバレッジを計測しておくのは重要です。
ちなみに、コードカバレッジについての考察を次の記事に書いています。
このようにとても重要な「テストの並列化」と「コードカバレッジの計測」ですが、両方を同時に実現するためには工夫が必要です。この記事では、次の2つを同時に実現する方法を記載します。
- pytestをCircleCIのコンテナで並列化することで、実行時間を改善する
- コードカバレッジを計測する
前提条件
次の環境で確認しています。
- Python: 3.8[2]
- Django: 4.0.1
- CircleCI
コードカバレッジの計測のためにCodecovを使用します。
Codecov以前Bashアップローダに問題があり、メルカリなどが被害を受けましたが、便利なサービスのため自分は引き続き利用しています(もちろん根本対応され、その後問題が起きてないことが前提ですが)。
動作確認には、以前も書いた自作のDjangoアプリケーションを使用しています。このなかでpytest絡みのライブラリのバージョンは次になります。
- pytest: 6.2.5
- coverage: 6.2
- pytest-cov: 3.0.0
- pytest-django: 4.5.2
- pytest-xdist: 2.5.0
- django_coverage_plugin: 2.0.2
CircleCIの設定
まず、現在使っている .circleci/config.yml
をそのまま掲載します。
version: 2.1
orbs:
codecov: codecov/codecov@3.2.0
commands:
install_packages:
steps:
# Download and cache dependencies
- restore_cache:
keys:
- v7-dependencies-{{ checksum "requirements/common.txt" }}-{{ checksum "requirements/development.txt" }}
- run:
command: |
set -eu
python -m venv venv
. venv/bin/activate
pip install -r requirements/development.txt
jobs:
build:
docker:
- image: cimg/python:3.8
environment:
- LANG: ja_JP.UTF-8
- LANGUAGE: ja_JP.UTF-8
resource_class: large
working_directory: ~/repo
steps:
- checkout
- install_packages
- run:
name: make build
command: make build
test:
docker:
- image: cimg/python:3.8
environment:
- LANG: ja_JP.UTF-8
- LANGUAGE: ja_JP.UTF-8
resource_class: large
parallelism: 3
working_directory: ~/repo
steps:
- checkout
- install_packages
- run:
name: make unit-test
command: |
export COVERAGE_FILE=.coverage.${CIRCLE_NODE_INDEX}
TESTFILES=$(circleci tests glob "apps/**/tests.py" "apps/**/test_*.py" "apps/**/*_tests.py" | circleci tests split --split-by=timings)
echo $TESTFILES
. venv/bin/activate
pytest -n 8 \
--cov-branch \
--cov=apps \
--cov=templates \
--junit-xml=test-results/junit.xml \
$TESTFILES
- save_cache:
paths:
- ./venv
key: v7-dependencies-{{ checksum "requirements/common.txt" }}-{{ checksum "requirements/development.txt" }}
- store_test_results:
path: test-results
- persist_to_workspace:
root: ~/repo
paths:
- .coverage.*
coverage:
docker:
- image: cimg/python:3.8
environment:
- LANG: ja_JP.UTF-8
- LANGUAGE: ja_JP.UTF-8
working_directory: ~/repo
steps:
- checkout
- install_packages
- attach_workspace:
at: ~/repo
- run:
name: combile coverage
command: |
. venv/bin/activate
coverage combine
coverage xml
- codecov/upload:
file: coverage.xml
workflows:
version: 2
main:
jobs:
- build
- test
- coverage:
requires:
- test
ポイントは次のとおりです。
- テスト用のジョブ(
test
) と、カバレッジ集計のためのジョブ(coverage
)を分け、後者を前者に依存させる -
parallelism: 3
: 3並列でコンテナを実行 - pytestのオプション
-
--cov-branch
: 「分岐網羅」を使用する(pytest-covのオプション) -
--cov=apps
,--cov=templates
: カバレッジを計測するパスを指定(pytest-covのオプション) -
--junit-xml
: JUnit形式のXMLを出力する
-
- store_test_results: CircleCIにテスト結果を格納する。このように
TESTS
タブが表示されます。
各コンテナで計測したカバレッジを保存する
テストを並列実行しつつカバレッジ計測を行うためには、並列実行した各コンテナで計測したカバレッジをマージする必要があります。そのために次のようにしています。
まず、カバレッジ計測ツール coverage
では、デフォルトで .coverage
というファイルに結果を書き込みます。しかし、デフォルトの設定だと全てのコンテナで同じファイル名のため、 persist_to_workspace
で保存する際に上書きされてしまいます。それを防ぐために、次のようにしてファイル名を変更しています。
export COVERAGE_FILE=.coverage.${CIRCLE_NODE_INDEX}
ここでは ${CIRCLE_NODE_INDEX}
という形で、ノードIDを指定しています。 すると、 .coverage
でなく .coverage.0
のようなファイル名で保存されます。CircleCIでなく他のCIサービスを使う際は、サービスごとに適切な環境変数を使用して、被らないようにしてください。
そして、次のステップでワークスペースに保存します。
jobs:
test:
steps:
- persist_to_workspace:
root: ~/repo
paths:
- .coverage.*
計測したカバレッジを1つにまとめる
次に、計測したカバレッジを1つでまとめます。これは次のステップで行われています。
jobs:
test:
steps:
- run:
name: combile coverage
command: |
. venv/bin/activate
coverage combine
coverage xml
coverage combine
コマンドで、 .coverage.*
ファイルを1つのファイルにマージします。そして、 coverage xml
コマンドで、カバレッジをXML形式で出力します。もしHTML形式がほしい場合は coverage html
コマンドを実行してください。
最後に、Codecovにアップロードします。
version: 2.1
orbs:
codecov: codecov/codecov@3.2.0
jobs:
coverage:
steps:
- codecov/upload:
file: coverage.xml
すると次のようにカバレッジが記録されます。
おわりに
これを書いているときに、CircleCIの無料プランで parallelism
がサポートされているのを知りました。毎月 $30 を払うのは正直キツかったので、ありがたいです。$10くらいなら気軽に払えるのですが・・・。
Discussion