【Python】coverageモジュールを使ってテストのカバー率を上げる


投稿日 2023年4月20日 >> 更新日 2023年4月27日

概要

coverageモジュールのCLIを使ってテストファイルを実行していきます。

実行されたテストファイルのデータに基づいて、テストカバー率のレポートを表示します。

テストのカバー率をHTMLファイルとして作成し分析します。

実行環境&使用ライブラリ

実行環境
Windows Subsystem for Linux
Python 3.10.0
使用ライブラリ ライセンス
coverage==7.2.3 Apache-2.0

coverageモジュールについて

coverageモジュールを使うことで、開発しているモジュールがどれほどテストできているか、どこの範囲をテストするべきかを測定することができます。

測定されたデータに基づいて、コンソール上にレポートとして表示することやHTMLファイルにしてブラウザ上で進捗を確認することができます。

以下の図は、HTMLページとしてカバレッジデータから作成された進捗データです。

「Module」の列は測定されたファイル名で「どのソースコードのテストが行われたか」、または「どこのソースコードのテストを行うべきか」を色分けされた状態で確認することができます。

以下は「greeting.py」のソースコードを確認した図です。

ソースコードの量が多い時などはこのように色分けされて可視化されるので大変ありがたいモジュールです。

テストファイルの作成については以下の記事で簡単に説明しています。

coverageの実装

pipを使ってインストールします。

$ pip install coverage

インストールが完了したらcoverageモジュールの「help」コマンドを実行して使用できるコマンドの詳細情報を表示してみます。

$ coverage help
Coverage.py, version 7.2.3 with C extension
Measure, collect, and report on code coverage in Python programs.

usage: coverage <command> [options] [args]

Commands:
    annotate    Annotate source files with execution information.
    combine     Combine a number of data files.
    debug       Display information about the internals of coverage.py
    erase       Erase previously collected coverage data.
    help        Get help on using coverage.py.
    html        Create an HTML report.
    json        Create a JSON report of coverage results.
    lcov        Create an LCOV report of coverage results.
    report      Report coverage stats on modules.
    run         Run a Python program and measure code execution.
    xml         Create an XML report of coverage results.

Use "coverage help <command>" for detailed help on any command.
Full documentation is at https://coverage.readthedocs.io/en/7.2.3

各コマンドの詳細情報については「coverage help」コマンドの後に特定のコマンドを指定して実行します。

$ coverage help run
Usage: coverage run [options] <pyfile> [program options]

Run a Python program, measuring code execution.

Options:
  -a, --append          Append coverage data to .coverage, otherwise it starts
                        clean each time.
  --branch              Measure branch coverage in addition to statement
                        coverage.
  --concurrency=LIBS    Properly measure code using a concurrency library.
                        Valid values are: eventlet, gevent, greenlet,
                        multiprocessing, thread, or a comma-list of them.
  --context=LABEL       The context label to record for this coverage run.
  --data-file=OUTFILE   Write the recorded coverage data to this file.
                        Defaults to '.coverage'. [env: COVERAGE_FILE]
  --include=PAT1,PAT2,...
                        Include only files whose paths match one of these
                        patterns. Accepts shell-style wildcards, which must be
                        quoted.
  -m, --module          <pyfile> is an importable Python module, not a script
                        path, to be run as 'python -m' would run it.
  --omit=PAT1,PAT2,...  Omit files whose paths match one of these patterns.
                        Accepts shell-style wildcards, which must be quoted.
  -L, --pylib           Measure coverage even inside the Python installed
                        library, which isn't done by default.
  -p, --parallel-mode   Append the machine name, process id and random number
                        to the data file name to simplify collecting data from
                        many processes.
  --source=SRC1,SRC2,...
                        A list of directories or importable names of code to
                        measure.
  --timid               Use a simpler but slower trace method. Try this if you
                        get seemingly impossible results!
  --debug=OPTS          Debug options, separated by commas. [env:
                        COVERAGE_DEBUG]
  -h, --help            Get help on this command.
  --rcfile=RCFILE       Specify configuration file. By default '.coveragerc',
                        'setup.cfg', 'tox.ini', and 'pyproject.toml' are
                        tried. [env: COVERAGE_RCFILE]

Full documentation is at https://coverage.readthedocs.io/en/7.2.3

テストを実行してカバレッジデータを作成する

ここで言うカバレッジデータとは「coverage」モジュールがテストを実行してその際に収集されたデータのことです。

収集されたデータはデフォルトだと「.coverage」として作成されます。

以下のようなディレクトリの構造上でテストを実行してみます。

.
├── .venv
├── goodbye.py
├── greeting.py
└── tests
    ├── __init__.py
    ├── test_goodbye.py
    └── test_greeting.py

テストを実行させるには、「coverage run」コマンドのオプション「-m(--module)」を指定し、テストを実行するフレームワークを設定します。

以下は各種テストフレームワークの実行例です。

「unittest」モジュールの実装に関してはこちらの記事で説明しています。

# unittestモジュールの例
$ coverage run -m unittest

# pytestモジュールの例
$ coverage run -m pytest

# Djangoプロジェクトの例
$ coverage run -m manage.py test

各自の例で実行しテストが成功すると、カレントディレクトリに「.coverage」が作成されているのが分かります。

.
├── .coverage # new
├── .venv
├── goodbye.py
├── greeting.py
└── tests
    ├── __init__.py
    ├── test_goodbye.py
    └── test_greeting.py

デフォルトでは「.coverage」として作成されますが、「coverage run」コマンドのオプション「--data-file=」もしくは「-p (--parallel-mode)」でカバレッジデータのファイル名を設定することができます。

「--data-file=」オプションでファイル名を設定すると任意のファイル名を決められます。

$ coverage run --data-file=.coverage.greeting -m unittest

ディレクトリ内を確認してみると「.coverage.greeting」ファイルが作成されているのが分かります。

.
├── .coverage
├── .coverage.greeting # new
├── .venv
├── goodbye.py
├── greeting.py
└── tests
    ├── __init__.py
    ├── test_goodbye.py
    └── test_greeting.py

「-p (--parallel-mode)」オプションを付けると、「.coverage」にドット繋ぎで実行しているマシン名に続きプロセスIDと乱数が付与されたファイル名が出来上がります。

$ coverage run -p -m unittest
.
├── .coverage
├── .coverage.greeting
├── .coverage.ideapad.355.490043 # new
├── .venv
├── goodbye.py
├── greeting.py
└── tests
    ├── __init__.py
    ├── test_goodbye.py
    └── test_greeting.py

上記の隠しファイルであるカバレッジデータの中身はどれも一緒です。

データが同じなのであまり意味はありませんが、「coverage combine」コマンドを実行するとカバレッジデータ同士を組み合わせることができます。

$ coverage combine
Combined data file .coverage.greeting
Skipping duplicate data .coverage.ideapad.355.490043

カバレッジデータのベースファイルとして「.coverage」に組み合わさりました。

.
├── .coverage
├── .venv
├── goodbye.py
├── greeting.py
└── tests
    ├── __init__.py
    ├── test_goodbye.py
    └── test_greeting.py

「coverage combine」コマンドは、異なるバージョンやOSで実行され収集されたカバレッジデータを結合することができます。

カバレッジデータのファイル名先頭が「.coverage...」となっている場合は自動で組み合わされますが、そうではない場合は「-a(--append)」オプションでカバレッジデータのファイル名を指定することができます。

$ coverage combine -a cov.data.1 cov.data.2

カバレッジデータを表示する

「coverage run」によって収集されたカバレッジデータを表示するには、「coverage report」もしくは、「coverage html」のコマンドを実行します。

コンソールにレポートを表示する

コマンドラインのコンソール上にカバレッジデータのレポートを表示します。

以下のディレクトリ構造上で「coverage run」によりテストランのデータを収集したとします。

.
├── .venv
├── goodbye.py
├── greeting.py
└── tests
    ├── __init__.py
    ├── test_goodbye.py
    └── test_greeting.py
$ coverage run -m unittest

するとカレントディレクトリには「.coverage」というカバレッジデータが作成されます。

.
├── .coverage # new
├── .venv
├── goodbye.py
├── greeting.py
└── tests
    ├── __init__.py
    ├── test_goodbye.py
    └── test_greeting.py

コンソール上でカバレッジデータのレポートを表示するには「coverage report」コマンドを実行します。

$ coverage report
Name                     Stmts   Miss  Cover
--------------------------------------------
goodbye.py                   9      3    67%
greeting.py                 22     12    45%
tests/__init__.py            0      0   100%
tests/test_goodbye.py        7      0   100%
tests/test_greeting.py       7      0   100%
--------------------------------------------
TOTAL                       45     15    67%

デフォルトでは実行環境下にある「.coverage」を読み込んで表示するそうです。

オプションの「-m(--show-missing)」を付けると、ソースコードのどの範囲がカバーされていないかを行番号で示してくれる列も表示されます。

$ coverage report -m
Name                     Stmts   Miss  Cover   Missing
------------------------------------------------------
goodbye.py                   9      3    67%   12-14
greeting.py                 22     12    45%   5, 12-13, 16-17, 20-21, 25-29
tests/__init__.py            0      0   100%
tests/test_goodbye.py        7      0   100%
tests/test_greeting.py       7      0   100%
------------------------------------------------------
TOTAL                       45     15    67%

「coverage report」のオプションも色々あります。

$ coverage help report
Usage: coverage report [options] [modules]

Report coverage statistics on modules.

Options:
  --contexts=REGEX1,REGEX2,...
                        Only display data from lines covered in the given
                        contexts. Accepts Python regexes, which must be
                        quoted.
  --data-file=INFILE    Read coverage data for report generation from this
                        file. Defaults to '.coverage'. [env: COVERAGE_FILE]
  --fail-under=MIN      Exit with a status of 2 if the total coverage is less
                        than MIN.
  --format=FORMAT       Output format, either text (default), markdown, or
                        total.
  -i, --ignore-errors   Ignore errors while reading source files.
  --include=PAT1,PAT2,...
                        Include only files whose paths match one of these
                        patterns. Accepts shell-style wildcards, which must be
                        quoted.
  --omit=PAT1,PAT2,...  Omit files whose paths match one of these patterns.
                        Accepts shell-style wildcards, which must be quoted.
  --precision=N         Number of digits after the decimal point to display
                        for reported coverage percentages.
  --sort=COLUMN         Sort the report by the named column: name, stmts,
                        miss, branch, brpart, or cover. Default is name.
  -m, --show-missing    Show line numbers of statements in each module that
                        weren't executed.
  --skip-covered        Skip files with 100% coverage.
  --no-skip-covered     Disable --skip-covered.
  --skip-empty          Skip files with no code.
  --debug=OPTS          Debug options, separated by commas. [env:
                        COVERAGE_DEBUG]
  -h, --help            Get help on this command.
  --rcfile=RCFILE       Specify configuration file. By default '.coveragerc',
                        'setup.cfg', 'tox.ini', and 'pyproject.toml' are
                        tried. [env: COVERAGE_RCFILE]

Full documentation is at https://coverage.readthedocs.io/en/7.2.3

以下のレポートを見る限り、レポートに含めなくても良いファイルがあったりします。

Name                     Stmts   Miss  Cover
--------------------------------------------
goodbye.py                   9      3    67%
greeting.py                 22     12    45%
tests/__init__.py            0      0   100%
tests/test_goodbye.py        7      0   100%
tests/test_greeting.py       7      0   100%
--------------------------------------------
TOTAL                       45     15    67%

例えば「__init__.py」や「test_....py」などです。

これらはテストを実行する際の「coverage run」の時点で除外することができますが、「coverage report」コマンドのオプションでも「含める・含めない」ファイルを指定できます。

  • 「--include=」オプションで表示したいファイルを指定する

ファイルを指定することで表示したいファイルのみ表示されます。

$ coverage report --include=goodbye.py,greeting.py
Name          Stmts   Miss  Cover
---------------------------------
goodbye.py        9      3    67%
greeting.py      22     12    45%
---------------------------------
TOTAL            31     15    52%
  • 「--omit=」オプションで除外したいファイルを相対パスを含めて指定する

レポートの表示で除外したいファイルがある場合は、相対パスで指定します。

$ coverage report --omit=tests/__init__.py
Name                     Stmts   Miss  Cover
--------------------------------------------
goodbye.py                   9      3    67%
greeting.py                 22     12    45%
tests/test_goodbye.py        7      0   100%
tests/test_greeting.py       7      0   100%
--------------------------------------------
TOTAL                       45     15    67%

複数の除外したいファイルがある場合は正規表現を使って検索することができます。

$ coverage report --omit=tests/*
Name          Stmts   Miss  Cover
---------------------------------
goodbye.py        9      3    67%
greeting.py      22     12    45%
---------------------------------
TOTAL            31     15    52%
  • 「--skip-covered」オプションでカバー率100%のファイル以外を表示する

カバー率(Cover)が100%のファイル以外を表示する場合。

$ coverage report --skip-covered
Name          Stmts   Miss  Cover
---------------------------------
goodbye.py        9      3    67%
greeting.py      22     12    45%
---------------------------------
TOTAL            45     15    67%

3 files skipped due to complete coverage.
  • 「--skip-empty」オプションでソースコードの無いファイル以外を表示する

コードが書かれていないファイル以外を表示します。

$ coverage report --skip-empty
Name                     Stmts   Miss  Cover
--------------------------------------------
goodbye.py                   9      3    67%
greeting.py                 22     12    45%
tests/test_goodbye.py        7      0   100%
tests/test_greeting.py       7      0   100%
--------------------------------------------
TOTAL                       45     15    67%

1 empty file skipped.

オプションは組み合わせて使えるので、柔軟にファイルの表示を行うことができます。

Webブラウザでレポートを表示する

Webブラウザ上でカバレッジデータのレポートを表示するには、カバレッジデータを元にHTMLファイルを生成します。

以下のディレクトリ構造上で「coverage run」によりテストランのデータを収集したとします。

.
├── .venv
├── goodbye.py
├── greeting.py
└── tests
    ├── __init__.py
    ├── test_goodbye.py
    └── test_greeting.py
$ coverage run -m unittest

するとカレントディレクトリには「.coverage」というカバレッジデータが作成されます。

.
├── .coverage # new
├── .venv
├── goodbye.py
├── greeting.py
└── tests
    ├── __init__.py
    ├── test_goodbye.py
    └── test_greeting.py

カバレッジデータを元に生成するHTMLレポートを作成するには、「coverage html」コマンドを実行します。

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

コンソールにも表示しているように、カレントディレクトリに「htmlcov」ディレクトリが作成され、「index.html」が配置されています。

.
├── .coverage
├── .venv
├── goodbye.py
├── greeting.py
├── htmlcov # new
│   ├── .gitignore
│   ├── coverage_html.js
│   ├── d_a44f0ac069e85531___init___py.html
│   ├── d_a44f0ac069e85531_test_goodbye_py.html
│   ├── d_a44f0ac069e85531_test_greeting_py.html
│   ├── favicon_32.png
│   ├── goodbye_py.html
│   ├── greeting_py.html
│   ├── index.html
│   ├── keybd_closed.png
│   ├── keybd_open.png
│   ├── status.json
│   └── style.css
└── tests
    ├── __init__.py
    ├── test_goodbye.py
    └── test_greeting.py

作成された「index.html」をブラウザで開くにはWindowsの場合ですとエクスプローラーかコマンドでリンクを作ってアクセスします。

以下はbashコマンドでリンクを作ってレポートを開いた例です。

※Windows Subsystem for Linuxの例

# readlinkコマンドでindex.htmlの絶対パスをlink変数に格納
$ link=`readlink -f htmlcov/index.html`

# wslpathコマンドでWindowsパスに変換してlink_win変数に格納
$ link_win=`wslpath -m $link`

# echoコマンドで「file:///」とlink_win変数を結合して表示する
$ echo file:///$link_win
file:///C:/Users/warik/Documents/PYTHON/cov/htmlcov/index.html

上記を行うと、ターミナルからブラウザへリンクすることができます。

アクセスされると、以下のようにレポートが可視化され各ファイルへアクセスできるようになります。

「greeting.py」にアクセスしてみると、カバー範囲等を確認することができます。

「coverage html」コマンドのオプションは以下です。

$ coverage help html
Usage: coverage html [options] [modules]

Create an HTML report of the coverage of the files.  Each file gets its own
page, with the source decorated to show executed, excluded, and missed lines.

Options:
  --contexts=REGEX1,REGEX2,...
                        Only display data from lines covered in the given
                        contexts. Accepts Python regexes, which must be
                        quoted.
  -d DIR, --directory=DIR
                        Write the output files to DIR.
  --data-file=INFILE    Read coverage data for report generation from this
                        file. Defaults to '.coverage'. [env: COVERAGE_FILE]
  --fail-under=MIN      Exit with a status of 2 if the total coverage is less
                        than MIN.
  -i, --ignore-errors   Ignore errors while reading source files.
  --include=PAT1,PAT2,...
                        Include only files whose paths match one of these
                        patterns. Accepts shell-style wildcards, which must be
                        quoted.
  --omit=PAT1,PAT2,...  Omit files whose paths match one of these patterns.
                        Accepts shell-style wildcards, which must be quoted.
  --precision=N         Number of digits after the decimal point to display
                        for reported coverage percentages.
  -q, --quiet           Don't print messages about what is happening.
  --show-contexts       Show contexts for covered lines.
  --skip-covered        Skip files with 100% coverage.
  --no-skip-covered     Disable --skip-covered.
  --skip-empty          Skip files with no code.
  --title=TITLE         A text string to use as the title on the HTML.
  --debug=OPTS          Debug options, separated by commas. [env:
                        COVERAGE_DEBUG]
  -h, --help            Get help on this command.
  --rcfile=RCFILE       Specify configuration file. By default '.coveragerc',
                        'setup.cfg', 'tox.ini', and 'pyproject.toml' are
                        tried. [env: COVERAGE_RCFILE]

Full documentation is at https://coverage.readthedocs.io/en/7.2.3
  • 「-d(--directory=)」オプションでHTMLレポートが格納されたディレクトリの配置場所やディレクトリ名を指定できます

デフォルトでは「htmlcov」というHTMLレポートのディレクトリが作成sれますが、オプションを使って「covrepo」というHTMLレポートのディレクトリを作成します。

$ coverage html -d covrepo
Wrote HTML report to covrepo/index.html
.
├── .coverage
├── .venv
├── covrepo # new
│   ├── ...
│   ├── index.html
│   ├── ...
├── goodbye.py
├── greeting.py
└── tests
    ├── __init__.py
    ├── test_goodbye.py
    └── test_greeting.py
  • 「--omit=」オプションで表示してほしくないファイルを除外します
$ coverage html --omit=tests/*
Wrote HTML report to htmlcov/index.html

  • 「--title=」オプションでHTMLレポートのタイトルを設定します
$ coverage html --title='Greeting report'
Wrote HTML report to htmlcov/index.html

他のも色々とありますが、「coverage report」コマンドのオプションと共通する部分が多いので試してみて下さい。

それでは以上となります。

最後までご覧いただきありがとうございました。