CLOVER🍀

That was when it all began.

Coverage.pyでPythonコードのカバレッジを取得する(+pytest-cov)

これは、なにをしたくて書いたもの?

そういえばPythonコードのカバレッジの取得方法を知らないですね、と思ったので少し見てみました。

Coverage.pyというものを使うようです。

Coverage.py

Coverage.pyのWebサイトはこちら。

Coverage.py — Coverage.py 7.13.5 documentation

GitHubリポジトリーはこちら。

GitHub - coveragepy/coveragepy: The code coverage tool for Python · GitHub

Coverage.pyはPythonコードのカバレッジを測定するツールです。

現在のバージョンは7.13.5で、Python 3.10以上をサポートしています。

Pythonのテストといえばpytestで、pytest向けにはpytest-covというプラグインがあるようです。

pytest-cov 7.1.0 documentation

GitHub - pytest-dev/pytest-cov: Coverage plugin for pytest. · GitHub

pytestと組み合わせる場合はpytest-covを使うのかと思いきや、Coverage.pyを見ると「多くの人はpytest-covを使うことを
選ぶが、通常は必要ない」とされています。

Many people choose to use the pytest-cov plugin, but for most purposes, it is unnecessary.

pytest-covの追加機能は以下のようです。

  • .coverageファイルの自動消去や結合、デフォルトレポート機能
  • パラメーター化を含む完全なテスト名をコンテキストに追加するなど、詳細なカバレッジコンテキストのサポート
  • リモートインタープリターを含むpytest-xdist(分散テスト)の機能を使用してカバレッジを取得する
  • Coverage.pyとpytestを組み合わせた時に発生するpytestの僅かな振る舞いの差を抑制し、pytestを一貫した動作にする

まずは使ってみるとしましょうか。

環境

今回の環境はこちら。

$ python3 --version
Python 3.12.3


$ uv --version
uv 0.11.9 (x86_64-unknown-linux-gnu)

Coverage.pyを使ってみる

それではCoverage.pyを使ってみましょう。

uvプロジェクトを作成。

$ uv init --vcs none coveragepy-getting-started
$ cd coveragepy-getting-started
$ rm main.py

pytestなどをインストール。

$ uv add --dev pytest mypy pyright ruff

Coverage.pyをインストール。

$ uv add --dev coverage

pyproject.toml

[project]
name = "coveragepy-getting-started"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

[dependency-groups]
dev = [
    "coverage>=7.13.5",
    "mypy>=1.20.2",
    "pyright>=1.1.409",
    "pytest>=9.0.3",
    "ruff>=0.15.12",
]

[tool.pytest.ini_options]
pythonpath = ["src"]

[tool.mypy]
strict = true
disallow_any_unimported = true
disallow_any_expr = true
disallow_any_explicit = true
warn_unreachable = true
pretty = true

[tool.pyright]
pythonVersion = "3.12"

typeCheckingMode = "strict"

deprecateTypingAliases = true
reportImplicitOverride = "error"
reportImplicitStringConcatenation = "error"
reportImportCycles = "error"
reportMissingSuperCall = "error"
reportPropertyTypeMismatch = "error"
reportUnnecessaryTypeIgnoreComment = "error"
reportUnreachable = "error"

インストールしたライブラリーなど。

$ uv tree
Resolved 16 packages in 1ms
coveragepy-getting-started v0.1.0
├── coverage v7.13.5 (group: dev)
├── mypy v1.20.2 (group: dev)
│   ├── librt v0.10.0
│   ├── mypy-extensions v1.1.0
│   ├── pathspec v1.1.1
│   └── typing-extensions v4.15.0
├── pyright v1.1.409 (group: dev)
│   ├── nodeenv v1.10.0
│   └── typing-extensions v4.15.0
├── pytest v9.0.3 (group: dev)
│   ├── iniconfig v2.3.0
│   ├── packaging v26.2
│   ├── pluggy v1.6.0
│   └── pygments v2.20.0
└── ruff v0.15.12 (group: dev)

ディレクトリーを作成。

$ mkdir src tests
$ touch tests/__init__.py

テスト対象を用意。

src/calc.py

class Calc:
    def plus(self, x: int, y: int) -> int:
        return x + y

    def minus(self, x: int, y: int) -> int:
        return x - y

    def multiply(self, x: int, y: int) -> int:
        return x * y

    def divide(self, x: int, y: int) -> float:
        return x / y

src/message.py

def star_format(message: str) -> str:
    return f"★★★{message}★★★"

テストコードを用意。

tests/test_calc.py

from calc import Calc


def test_plus() -> None:
    calc = Calc()
    assert calc.plus(1, 3) == 4


def test_minus() -> None:
    calc = Calc()
    assert calc.minus(5, 3) == 2


def test_divide() -> None:
    calc = Calc()
    assert calc.divide(10, 5) == 2

tests/test_message.py

from message import star_format


def test_format() -> None:
    assert star_format("Hello World") == "★★★Hello World★★★"

テストが足りていませんが、まあ意図的です。

では、カバレッジを取得してみましょう。

バージョン確認。

$ uv run coverage --version
Coverage.py, version 7.13.5 with C extension
Full documentation is at https://coverage.readthedocs.io/en/7.13.5

pytestを使ってテストを実行。この時にcoverage runを経由することになります。

$ uv run coverage run -m pytest

実行すると、.coverageというファイルができます。

.coverageファイルがあると、レポート表示ができます。

$ uv run coverage report
Name                    Stmts   Miss  Cover
-------------------------------------------
src/calc.py                 9      1    89%
src/message.py              2      0   100%
tests/__init__.py           0      0   100%
tests/test_calc.py         10      0   100%
tests/test_message.py       3      0   100%
-------------------------------------------
TOTAL                      24      1    96%

カバーしていない箇所を表示するには、--show-missingまたは-mオプションを指定します。

$ uv run coverage report -m
Name                    Stmts   Miss  Cover   Missing
-----------------------------------------------------
src/calc.py                 9      1    89%   9
src/message.py              2      0   100%
tests/__init__.py           0      0   100%
tests/test_calc.py         10      0   100%
tests/test_message.py       3      0   100%
-----------------------------------------------------
TOTAL                      24      1    96%

HTMLレポートも作成できます。

$ uv run coverage html
Wrote HTML report to htmlcov/index.html

htmlcovというディレクトリーが作成され、この中にHTMLレポートが配置されます。

こんなレポートになりました。

設定もしてみましょう。

Configuration reference — Coverage.py 7.13.5 documentation

pyproject.tomlにこんな感じに追加。

[tool.coverage.run]
branch = true
command_line = "-m pytest"

[tool.coverage.report]
show_missing = true

omit = [
     "tests/*"
]

ブランチカバレッジを取得する、coverage runのみを指定した時にpytestを実行するようにする、レポート表示の際にカバー
していない箇所を表示する、testsディレクトリーはレポートから除外する、です。

結果を見た方が早いですね。

$ uv run coverage run
=========================================================================== test session starts ============================================================================
platform linux -- Python 3.12.3, pytest-9.0.3, pluggy-1.6.0
rootdir: /path/to/coveragepy-getting-started
configfile: pyproject.toml
collected 4 items

tests/test_calc.py ...                                                                                                                                               [ 75%]
tests/test_message.py .                                                                                                                                              [100%]

============================================================================ 4 passed in 0.02s =============================================================================


$ uv run coverage report
Name             Stmts   Miss Branch BrPart  Cover   Missing
------------------------------------------------------------
src/calc.py          9      1      0      0    89%   9
src/message.py       2      0      0      0   100%
------------------------------------------------------------
TOTAL               11      1      0      0    91%

こんなところでしょうか。

pytest-covを使ってみる

せっかくなので、pytest-covも使ってみましょう。

お題はCoverage.pyをそのまま使う時と同じにして、変化を見るようにしてみましょう。

uvプロジェクトの作成。

$ uv init --vcs none pytest-cov-getting-started
$ cd pytest-cov-getting-started
$ rm main.py

pytestなどの追加。

$ uv add --dev pytest mypy pyright ruff

pytest-covのインストール。

$ uv add --dev pytest-cov

pyproject.toml

[project]
name = "pytest-cov-getting-started"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

[dependency-groups]
dev = [
    "mypy>=1.20.2",
    "pyright>=1.1.409",
    "pytest>=9.0.3",
    "pytest-cov>=7.1.0",
    "ruff>=0.15.12",
]

[tool.pytest.ini_options]
pythonpath = ["src"]

[tool.mypy]
strict = true
disallow_any_unimported = true
disallow_any_expr = true
disallow_any_explicit = true
warn_unreachable = true
pretty = true

[tool.pyright]
pythonVersion = "3.12"

typeCheckingMode = "strict"

deprecateTypingAliases = true
reportImplicitOverride = "error"
reportImplicitStringConcatenation = "error"
reportImportCycles = "error"
reportMissingSuperCall = "error"
reportPropertyTypeMismatch = "error"
reportUnnecessaryTypeIgnoreComment = "error"
reportUnreachable = "error"

インストールしたライブラリーの一覧。

$ uv tree
Resolved 17 packages in 1ms
pytest-cov-getting-started v0.1.0
├── mypy v1.20.2 (group: dev)
│   ├── librt v0.10.0
│   ├── mypy-extensions v1.1.0
│   ├── pathspec v1.1.1
│   └── typing-extensions v4.15.0
├── pyright v1.1.409 (group: dev)
│   ├── nodeenv v1.10.0
│   └── typing-extensions v4.15.0
├── pytest v9.0.3 (group: dev)
│   ├── iniconfig v2.3.0
│   ├── packaging v26.2
│   ├── pluggy v1.6.0
│   └── pygments v2.20.0
├── pytest-cov v7.1.0 (group: dev)
│   ├── coverage v7.13.5
│   ├── pluggy v1.6.0
│   └── pytest v9.0.3 (*)
└── ruff v0.15.12 (group: dev)

pytest-covにCoverage.pyもpytestも含まれています。

テスト対象およびテストコードはまったく同じなので省略。

pytestのヘルプを見ると

$ uv run pytest --help

カバレッジに関する内容が追加されています。

coverage reporting with distributed testing support:
  --cov=[SOURCE]        Path or package name to measure during execution (multi-allowed). Use --cov= to not do any source filtering and record everything.
  --cov-reset           Reset cov sources accumulated in options so far.
  --cov-report=TYPE     Type of report to generate: term, term-missing, annotate, html, xml, json, markdown, markdown-append, lcov (multi-allowed). term, term-missing may
                        be followed by ":skip-covered". annotate, html, xml, json, markdown, markdown-append and lcov may be followed by ":DEST" where DEST specifies the
                        output location. Use --cov-report= to not generate any output.
  --cov-config=PATH     Config file for coverage. Default: .coveragerc
  --no-cov-on-fail      Do not report coverage if test run fails. Default: False
  --no-cov              Disable coverage report completely (useful for debuggers). Default: False
  --cov-fail-under=MIN  Fail if the total coverage is less than MIN.
  --cov-append          Do not delete coverage but append to current. Default: False
  --cov-branch          Enable branch coverage.
  --cov-precision=COV_PRECISION
                        Override the reporting precision.
  --cov-context=CONTEXT
                        Dynamic contexts to use. "test" for now.

pytestをそのまま実行するだけでは、特になにも変わりません。

$ uv run pytest
=========================================================================== test session starts ============================================================================
platform linux -- Python 3.12.3, pytest-9.0.3, pluggy-1.6.0
rootdir: /path/to/pytest-cov-getting-started
configfile: pyproject.toml
plugins: cov-7.1.0
collected 4 items

tests/test_calc.py ...                                                                                                                                               [ 75%]
tests/test_message.py .                                                                                                                                              [100%]

============================================================================ 4 passed in 0.01s =============================================================================

--covオプションをつけると、カバレッジを取得するようになります。

$ uv run pytest --cov
=========================================================================== test session starts ============================================================================
platform linux -- Python 3.12.3, pytest-9.0.3, pluggy-1.6.0
rootdir: /path/to/pytest-cov-getting-started
configfile: pyproject.toml
plugins: cov-7.1.0
collected 4 items

tests/test_calc.py ...                                                                                                                                               [ 75%]
tests/test_message.py .                                                                                                                                              [100%]

============================================================================== tests coverage ==============================================================================
_____________________________________________________________ coverage: platform linux, python 3.12.3-final-0 ______________________________________________________________

Name                    Stmts   Miss  Cover
-------------------------------------------
src/calc.py                 9      1    89%
src/message.py              2      0   100%
tests/__init__.py           0      0   100%
tests/test_calc.py         10      0   100%
tests/test_message.py       3      0   100%
-------------------------------------------
TOTAL                      24      1    96%
============================================================================ 4 passed in 0.04s =============================================================================

--cov=[パス]で指定すると、その対象に絞ってカバレッジを表示します。
--cov [パス]でも可

$ uv run pytest --cov=src
=========================================================================== test session starts ============================================================================
platform linux -- Python 3.12.3, pytest-9.0.3, pluggy-1.6.0
rootdir: /path/to/pytest-cov-getting-started
configfile: pyproject.toml
plugins: cov-7.1.0
collected 4 items

tests/test_calc.py ...                                                                                                                                               [ 75%]
tests/test_message.py .                                                                                                                                              [100%]

============================================================================== tests coverage ==============================================================================
_____________________________________________________________ coverage: platform linux, python 3.12.3-final-0 ______________________________________________________________

Name             Stmts   Miss  Cover
------------------------------------
src/calc.py          9      1    89%
src/message.py       2      0   100%
------------------------------------
TOTAL               11      1    91%
============================================================================ 4 passed in 0.03s =============================================================================

こう見ると、pytest-covはpytestの実行時に合わせてカバレッジの処理もやってしまうというプラグインになりますね。

--cov-report=[タイプ]でレポートの種類を指定できます。--cov-report=term-missingではカバーしていない箇所を表示します。

$ uv run pytest --cov=src --cov-report=term-missing
=========================================================================== test session starts ============================================================================
platform linux -- Python 3.12.3, pytest-9.0.3, pluggy-1.6.0
rootdir: /path/to/pytest-cov-getting-started
configfile: pyproject.toml
plugins: cov-7.1.0
collected 4 items

tests/test_calc.py ...                                                                                                                                               [ 75%]
tests/test_message.py .                                                                                                                                              [100%]

============================================================================== tests coverage ==============================================================================
_____________________________________________________________ coverage: platform linux, python 3.12.3-final-0 ______________________________________________________________

Name             Stmts   Miss  Cover   Missing
----------------------------------------------
src/calc.py          9      1    89%   9
src/message.py       2      0   100%
----------------------------------------------
TOTAL               11      1    91%
============================================================================ 4 passed in 0.03s =============================================================================

こんな感じで繰り返し指定も可能です。この例ではHTMLレポートも合わせて生成しています。

$ uv run pytest --cov=src --cov-report=term-missing --cov-report=html
=========================================================================== test session starts ============================================================================
platform linux -- Python 3.12.3, pytest-9.0.3, pluggy-1.6.0
rootdir: /path/to/pytest-cov-getting-started
configfile: pyproject.toml
plugins: cov-7.1.0
collected 4 items

tests/test_calc.py ...                                                                                                                                               [ 75%]
tests/test_message.py .                                                                                                                                              [100%]

============================================================================== tests coverage ==============================================================================
_____________________________________________________________ coverage: platform linux, python 3.12.3-final-0 ______________________________________________________________

Name             Stmts   Miss  Cover   Missing
----------------------------------------------
src/calc.py          9      1    89%   9
src/message.py       2      0   100%
----------------------------------------------
TOTAL               11      1    91%
Coverage HTML written to dir htmlcov
============================================================================ 4 passed in 0.04s =============================================================================

その他の設定も含めて、ドキュメントはこちら。

Configuration - pytest-cov 7.1.0 documentation

pyproject.tomlではtool.pytest.ini_optionsセクションのaddoptsで指定できます。

[tool.pytest.ini_options]
pythonpath = ["src"]
addopts = "--cov=src --cov-branch --cov-report=term-missing --cov-report=html"

項目によってはtool.coverage.runtool.coverage.reportなどで指定もできますが、どうも中途半端になるので今回はすべて
addoptsにまとめました。

これでuv run pytestを実行するだけでカバレッジの取得とレポート出力まで行われます。

$ uv run pytest
=========================================================================== test session starts ============================================================================
platform linux -- Python 3.12.3, pytest-9.0.3, pluggy-1.6.0
rootdir: /path/to/pytest-cov-getting-started
configfile: pyproject.toml
plugins: cov-7.1.0
collected 4 items

tests/test_calc.py ...                                                                                                                                               [ 75%]
tests/test_message.py .                                                                                                                                              [100%]

============================================================================== tests coverage ==============================================================================
_____________________________________________________________ coverage: platform linux, python 3.12.3-final-0 ______________________________________________________________

Name             Stmts   Miss Branch BrPart  Cover   Missing
------------------------------------------------------------
src/calc.py          9      1      0      0    89%   9
src/message.py       2      0      0      0   100%
------------------------------------------------------------
TOTAL               11      1      0      0    91%
Coverage HTML written to dir htmlcov
============================================================================ 4 passed in 0.04s =============================================================================

おわりに

Coverage.pyを使ってPythonコードのカバレッジを取得してみました。

pytest-covについては、今回のような使い方だと確かにCoverage.pyを直接使うでも構わないかも…とは思いました。

とりあえず、基本的な使い方はわかりました。