はじめに
Pythonテスティングフレームワークであるpytest, unittestなどで、カバレッジを計測する際にはcoverage.pyというライブラリが内部的に利用されています。
coverage.py v7.11.3までは、分岐網羅率(以下、C1カバレッジ)を単体で確認することはできず、C0カバレッジ単体か、C0/C1カバレッジの合算値しか確認できませんでした。
そこで、C0/C1カバレッジを分けて表示できるようにするPull Requestを作成し、コントリビュートしました
(この変更は、2025/11/18リリースのv7.12.0に取り込まれています。)
この記事では、pytest/unittestでC0/C1カバレッジを分けて確認する手順と、coverage.pyの挙動に関連するポイントについて説明します。
目次
想定読者
- pytest / unittest でC0カバレッジとC1カバレッジを分けて確認したい方
-
--cov-branchオプションを使っているが、「これ本当にC1カバレッジなのか?」と疑問に感じたことがある方 - pytestでカバレッジがどのように計測されているかを、ざっくり把握しておきたい方
TL;DR
- pytest-covなどを使っていて「これ本当に分岐網羅?」と感じている場合は、coverage.py v7.12.0以降にアップデートすると、C0とC1を分けて確認できるようになります
- 未導入の場合は、pytest-cov を導入して
--cov-branchを付けて実行すると、HTMLレポートでC0とC1を分けて確認できるようになります
※ pytestのカバレッジは内部的にcoverage.pyによって計測されています
用語定義
本記事では、以下の用語を使用します。
- カバレッジ
- テストによって、どの程度コードが実行・検証されたかを表す割合
- C0カバレッジ = 命令網羅率
- C1カバレッジ = 分岐網羅率
これまでのcoverage.pyの仕様
coverage.py v7.11.3までは、--branchやpytest-covの--cov-branchを指定したときに表示されるカバレッジは、C1カバレッジ単体の値ではありませんでした。
この時に表示されるカバレッジは、C0とC1を合算したものになります。
式としては、以下のようになります。
$$
\text{カバレッジ} =
\frac{\text{実行された命令数} + \text{実行された分岐数}}
{\text{総命令数} + \text{総分岐数}}
$$
一方、本来のC1カバレッジは、以下のように分岐だけを分母・分子にして計算します。
$$
\text{C1カバレッジ} =
\frac{\text{実行された分岐数}}
{\text{総分岐数}}
$$
つまり、--cov-branchで表示される値は、純粋なC1カバレッジではなく、「C0とC1カバレッジの合算値」でした。
例えば、以下のPythonコードを考えます。
def is_minor(age: int) -> bool:
"""Return True if the age is under 18."""
if age < 18:
return True
else:
return False
18歳未満かどうかで分岐するコードですが、テストコードでは、18歳未満のケースだけを確認します。
"""Tests for is_minor function."""
from age import is_minor
def test_is_minor_with_child_age() -> None:
"""18歳未満の場合、Trueを返すこと."""
assert is_minor(17) is True
このテストでは、
- age < 18がTrueになる分岐
- age < 18がFalseになる分岐
のうち、True側しか通っていません。
つまり、本来のC1カバレッジは50%です。
しかし、v7.11.3まででは、このケースのカバレッジは67%と表示されます。
pytest --cov=. --cov-branch --cov-report html
このように、--cov-branchを利用していても、
表示される値は、純粋なC1カバレッジではありませんでした。
この問題に対する対応
この問題を解消するため、C0/C1カバレッジを個別に表示できるようにするPull Requestを作成し、coverage.pyにコントリビュートしました。
-
HTMLレポート対応:
https://github.com/coveragepy/coveragepy/pull/2085 -
JSONレポート対応:
https://github.com/coveragepy/coveragepy/pull/2090
これらの変更は、2025/11/18リリースのcoverage.py v7.12.0に取り込まれています。
これにより、命令網羅(C0)と分岐網羅(C1)を個別に確認できるようになりました。
カバレッジ取得の手順
pytest-covを導入していない場合
まず、pytest-covをインストールします。
pipの場合:
pip install pytest-cov
uvの場合:
uv add --dev pytest-cov
その後、以下のコマンドでカバレッジを計測できます。
pytest --cov=. --cov-branch --cov-report html
※ uv環境の場合は uv run pytest を利用してください。
実行後、HTMLレポートがhtmlcov/index.htmlに出力されます。
coverage.py v7.12.0以降では、HTMLレポート上でC0/C1カバレッジを個別に確認できるようになります。
pytest-covを導入済みの場合
pytest-covを既に利用している場合は、coverage.pyをv7.12.0以降へ更新します。
pipの場合:
pip install -U coverage
uvの場合:
uv add --dev coverage@latest
バージョン確認は以下で行えます。
coverage --version
アップデート後、再度pytestを実行します。
pytest --cov=. --cov-branch --cov-report html
※ uv環境の場合は uv run pytest を利用してください。
pytestのカバレッジ取得の仕組み
pytestで--covオプションを利用した場合、内部的には以下のような構成でカバレッジが計測されています。
pytest
↓
pytest-cov
↓
coverage.py
つまり、pytestで表示されるカバレッジの仕様や表示形式は、最終的にはcoverage.pyの仕様に依存しています。
そのため、今回の変更もpytest-cov側ではなく、coverage.py側へのコントリビュートによって実現されています。
まとめ
-
--cov-branchで表示される値は、以前は命令網羅と分岐網羅の合算値だった - coverage.py v7.12.0以降では、C0/C1カバレッジを個別に確認できる
- pytestのカバレッジは内部的にcoverage.pyによって計測されている
「分岐網羅を確認しているつもりだったが、実際には合算値だった」という違和感を持っていた方は、coverage.pyのバージョンを確認してみてください。
同じ違和感を持っていた方の参考になれば幸いです。

