2021年09月27日 - Tomo Masakura    

GitLabでコードカバレッジ

.NETのコードカバレッジをGitLab上で分析する

これは.NETでコードカバレッジを取得して、それをGitLabのマージリクエストで確認する方法を書いたブログです。

このブログ及びサンプルプロジェクトは.NETを利用しています。ですが、他の言語でもGitLab側の設定はあまり変わりません。他の言語でも参考になると思います。

GitLabでコードカバレッジを確認する

GitLabのマージリクエストなどでコードカバレッジを確認する方法はいくつかあります。このブログではそれら全ての設定方法を解説していますので、長文になります。ブログを読みながら、設定をしてこうなるのかーと理解するのはかなり大変なので、サンプルプロジェクトを用意して、先に概要をつかめるようにしています。

総カバレッジ率を確認する

お試しのマージリクエストを開いてください。パイプライン #.... のすぐ下辺りで総カバレッジ率を確認できます。

総カバレッジ率 総カバレッジ率

総カバレッジ率は、マージリクエスト以外にも、GitLabプロジェクトのサイドメニューの分析->リポジトリから推移グラフで確認できます。

総カバレッジ率の推移グラフ 総カバレッジ率の推移グラフ

(サンプルプロジェクトの推移グラフはデータが少なくて面白くないので、GitLabの公式ドキュメントから画像を引用しています)

変更されたコードのコードカバレッジを確認する

マージリクエストの変更タブにあるコード差分でコードカバレッジを確認できます。

マージリクエストのコード差分でのコードカバレッジ マージリクエストのコード差分でのコードカバレッジ

次の手順で試してみてください。

  1. お試しのマージリクエストを開きます
  2. 変更タブをクリックします
  3. 緑や赤の縦線がコード差分に表示されるまでリロードしてください

詳細なコードカバレッジレポートを確認する

コードカバレッジの生のデータを見ても、わかる人は少ないと思います。人間が読んでもわかりやすいレポート(一般的にHTML形式)に変換するのが一般的です。それをGitLabから確認できます。

コードカバレッジの概要 コードカバレッジの概要

クラスごとの詳細なコードカバレッジ クラスごとの詳細なコードカバレッジ

次の手順で試してみてください。

  1. お試しのマージリクエストを開きます
  2. 1 件の公開されたアーティファクトを表示をクリックして開きます
  3. coverage -> coverage -> index.html とクリックすると、詳細なカバレッジレポートが表示されます

単体テスト結果を確認する(おまけ)

GitLabは単体テスト結果を表示する機能があります。コードカバレッジとは違いますが、せっかくなのでまとめて紹介します。

次の手順で試してみてください。

  1. お試しのマージリクエストを開きます
  2. テストの要約...のところの展開をクリックすると、マージリクエストで変わったテスト結果だけが表示されます

マージリクエストの単体テスト結果要約 マージリクエストの単体テスト結果要約

レポート全体を見るをクリックすると、全ての詳細な単体テスト結果にアクセスできます。

パイプラインの単体テスト結果要約 パイプラインの単体テスト結果要約

テスト結果の単体詳細 テスト結果の単体詳細

マージリクエストからだけでなく、各パイプラインのテストタブでも確認できます。

GitLabに設定する

それでは.NETのプロジェクトを作成して、コードカバレッジをGitLabで確認できるように設定していきます。

.NETプロジェクトを作る

消費税の計算をする機能を単体テストを行い、そのコードカバレッジを取ります。

現時点の消費税の仕様は次のとおりです。

  • 消費税率は一律10%とする
  • 小数以下は切り捨てる

後で、これに軽減税率(8%)や消費税なしを仕様を追加し、そのコードカバレッジを分析します。

Visual Studioソリューションの構成は次のとおりです。

プロジェクト名  
ConsumptionTax 消費税の計算をするクラスライブラリプロジェクト
ConsumptionTax.Tests ConsumptionTax クラスライブラリのテストプロジェクト

早速プロジェクトを作りましょう。

mkdir gitlab-coverage-dotnet-sample
cd gitlab-coverage-dotnet-sample
dotnet new sln
dotnet new classlib -n ConsumptionTax
dotnet sln add ConsumptionTax
dotnet new xunit -n ConsumptionTax.Tests
dotnet sln add ConsumptionTax.Tests
dotnet add ConsumptionTax.Tests reference ConsumptionTax

ConsumptionTax\消費税.cs クラスを作成します。(この時点では消費税率は一律10%です)

using System;

namespace ConsumptionTax
{
    public class 消費税
    {
        public static int 消費税額(int 税抜金額)
        {
            var 税額 = 税抜金額 * 0.10m;
            return (int) Math.Floor(税額);
        }
    }
}

ConsumptionTax.Tests\消費税のテスト.cs クラスを作成します。

using Xunit;

namespace ConsumptionTax.Tests
{
    public class 消費税のテスト
    {
        [Fact]
        public void 税抜100円の消費税額は10()
        {
            var actual = 消費税.消費税額(100);

            Assert.Equal(10, actual);
        }
    }
}

クラスを追加したら、単体テストを実行します。

dotnet test

次のように全てのテストが成功することを確認してください。

...

テスト実行を開始しています。お待ちください...
合計 1 個のテスト ファイルが指定されたパターンと一致しました。

成功!   -失敗:     0、合格:     2、スキップ:     0、合計:     1、期間: < 1 ms - ...

あとはGitLabにプロジェクトを作成してこのVisual StudioソリューションをGitLabのプロジェクトにプッシュしてください。

GitLabで単体テストの実行

GitLabには単体テストの結果を表示する機能があります。コードカバレッジではありませんが、せっかくですのでこの機能の設定方法も紹介します。

マージリクエストでの単体テスト結果要約 マージリクエストでの単体テスト結果要約

単体テスト結果詳細 単体テスト結果詳細

GitLabで単体テストの結果を確認するためには、レポートがJUnit形式でなければなりませんので、JUnit形式で出力するテストレポーターを組み込みます。

テストレポーターはいくつかありますが、JunitXml.TestLoggerはGitLab CIでの設定例も紹介されているので、採用します。

まずはテストプロジェクトにJunitXml.TestLoggerを追加します。

dotnet add ConsumptionTax.Tests package JunitXml.TestLogger

公式サイトのGitLab CI/CD の推奨設定を参考にしてJUnit形式のレポートを出力します。次のコマンドを実行します。

dotnet test --test-adapter-path:. --logger:"junit;LogFilePath=../artifacts/{assembly}-test-result.xml;MethodFormat=Class;FailureBodyFormat=Verbose"

ファイルartifacts\ConsumptionTax.Tests-test-result.xmlが出力されていることを確認してください。(レポートファイルの出力先は、カレントディレクトリではなく、各テストプロジェクトからの相対ディレクトリなことに注意してください)

そして、GitLab CIに組み込みます。.gitlab-ci.ymlファイルを次のように作成します。

image: mcr.microsoft.com/dotnet/sdk:5.0

test:
  script:
    - |
      dotnet test \
        --test-adapter-path:. \
        --logger:"junit;LogFilePath=${CI_PROJECT_DIR}/artifacts/{assembly}-test-result.xml;MethodFormat=Class;FailureBodyFormat=Verbose"
  artifacts:
    reports:
      junit:
        - ./artifacts/*test-result.xml

artifactsキーワードを使うと、アーティファクトをアップロードしてGitLabで確認できるようになります。reportsキーワードは特別なアーティファクトのアップロードに用いられます。例えば、junitキーワードを使ってJUnit形式のレポートをアップロードをすると、そのファイルをGitLabが解析し、テスト結果をGitLabで確認することができるようになります。

もちろん、junitキーワード以外にも、コードカバレッジレポートをアップロードするためのcoberturaキーワードなど、他にもあります

.gitlab-ci.ymlファイルの作成が終わったら、コミットしてGitLabにプッシュしてください。GitLab CIパイプラインが開始され、単体テストが実行されます。

GitLabの該当プロジェクトのサイドメニューからCI/CD ->パイプラインをクリックし、一番上にある最新のパイプラインをクリックしてください。そこのテストタブを開くと単体テストの詳細を見ることができます。

パイプラインのテストタブ パイプラインのテストタブ

コードカバレッジを取る

.NETの単体テストでコードカバレッジを取る方法は、.NETの公式ドキュメントの単体テストにコードカバレッジを使用するに書かれています。こちらを参考にします。

次のコマンドを実行します。

dotnet test --collect:"XPlat Code Coverage"

ファイルConsumptionTax.Tests\TestResults\....\coverage.cobertura.xmlが出力されているはずです。

このコードカバレッジのXMLファイルは生データで、そのままでは役に立ちません。GitLabでわかりやすく表示する設定をしていきます。

マージリクエストのコード差分でのコードカバレッジ

GitLabのマージリクエストの変更タブにあるコード差分でコードカバレッジを確認する設定をします。

マージリクエストのコード差分でのコードカバレッジ マージリクエストのコード差分でのコードカバレッジ

単体テスト結果のときのように、reports:coberturaにコードカバレッジのXMLファイルを添付するだけです。

.gitlab-ci.ymltestジョブを次のように書き換えます。

test:
  script:
    - dotnet tool restore
    - |
      dotnet test \
        --test-adapter-path:. \
        --logger:"junit;LogFilePath=${CI_PROJECT_DIR}/artifacts/{assembly}-test-result.xml;MethodFormat=Class;FailureBodyFormat=Verbose" \
        --collect:"XPlat Code Coverage"
  artifacts:
    reports:
      junit:
        - ./artifacts/*test-result.xml
      cobertura:
        - ./**/coverage.cobertura.xml

ただし、.NETのコードカバレッジツールが出力したcoverage.cobertura.xmlをGitLabがうまく処理できない場合があります。それは、コードカバレッジ対象のクラスが一つしかないときです。実際の開発プロジェクトではありえませんが、このプロジェクトは該当します。

そのバグ回避のために、ConsumptionTax\Dummy.csファイルを作成します。(実際の開発プロジェクトでは不要です)

namespace ConsumptionTax
{
    public class Dummy
    {
        // coverlet.collector によるカバレッジの取得は
        // カバレッジ対象にできるクラスファイルが複数ないと
        // GitLab が認識できる形式にならない。
        // そのためのダミーファイル。

        public void DummyMethod()
        {
        }
    }
}

修正が終わったら、コミットしてGitLabにプッシュしてください。GitLabプロジェクトのサイドメニューのCI/CD ->パイプラインをクリックして、該当パイプラインの一番右のボタン->Download test:coberturaをクリックしてください。コードカバレッジのXMLファイルをダウンロードできます。

coverage.covertura.xmlのダウンロード coverage.covertura.xmlのダウンロード

これでマージリクエストのコード差分でコードカバレッジを確認できるようになりました。あとでマージリクエストを作成して、分析していきます。

詳細なコードカバレッジレポート

マージリクエストのコード差分で確認できるコードカバレッジは限定的です。変更されていないファイルのコードカバレッジは確認できませんし、クラスごとのカバレッジ率も確認できません。

コードカバレッジを詳細なHTML形式のレポートに変換し、それをGitLabで確認できるようにする設定をします。

コードカバレッジの概要 コードカバレッジの概要

.NETの公式ドキュメントの単体テストにコードカバレッジを使用するReportGeneratorを利用したHTML化の方法が書かれていますので、これに参考にします。

ReportGeneratorは.NET Tools版がありますので、これを利用します。

まずはプロジェクトローカルに.NET Toolsをインストールできるようにします。

dotnet new tool-manifest

次にReportGeneratorの.NET Tools版をインストールします。

dotnet tool install dotnet-reportgenerator-globaltool

わかりやすいHTMLレポートを作成します。

dotnet reportgenerator -reports:./**/coverage.cobertura.xml -targetdir:coverage -reporttypes:Html

coverageフォルダーにファイルindex.htmlを始めとしたファイルが生成されています。ファイルcoverage\index.htmlをダブルクリックしてウェブブラウザーで表示してくみてください。

最後にこのHTMLファイルをGitLab アーティファクトにアップロードして、GitLab上で確認できるようにします。

なお、この機能はGitLab Pagesを必要とします。自前のGitLabの場合はGitLab Pagesが使えるか事前にご確認ください。

.gitlab-ci.ymltestジョブを次のように書き換えます。

test:
  script:
    - dotnet tool restore
    - |
      dotnet test \
        --test-adapter-path:. \
        --logger:"junit;LogFilePath=${CI_PROJECT_DIR}/artifacts/{assembly}-test-result.xml;MethodFormat=Class;FailureBodyFormat=Verbose" \
        --collect:"XPlat Code Coverage"
  after_script:
    - |
      dotnet reportgenerator \
        -reports:./**/coverage.cobertura.xml \
        -targetdir:coverage \
        -reporttypes:Html
  artifacts:
    when: always
    expose_as: coverage
    paths:
      - coverage/
    reports:
      junit:
        - ./artifacts/*test-result.xml
      cobertura:
        - ./**/coverage.cobertura.xml

when: alwaysおよびafter_scriptは失敗した単体テストがあってもレポートの作成とアーティファクトへのアップロードをするためです。

pathsキーワードで、GitLabにアップロードするアーティファクトにHTMLレポートのフォルダーを指定して、レポートの画像ファイルも含めて全て指定します。

expose_asキーワードはマージリクエストにアーティファクトへ直接ジャンプするリンクを表示します。今はマージリクエストを作っていないので確認できませんが、次のスクリーンショットのようになります。

マージリクエストのカバレッジレポートへのリンク マージリクエストのカバレッジレポートへのリンク

GitLabの該当プロジェクトのサイドメニューでCI/CD ->パイプラインをクリックして、該当パイプラインを表示します。次に、testジョブを開いてブラウズindex.htmlファイルを探して表示してください。詳細なコードカバレッジレポートが表示されます。

アーティファクトにアップロードされたレポートは、そのうち削除されます。削除されないようにするには、ジョブページの右にある維持ボタンをクリックしてください。もしくは、.gitlab-ci.ymlファイルのexpire_inキーワードで期限を設定できます。

余談ですが、詳細なレポートグラフをHTMLに変換してGitLabで確認する方法はコードカバレッジだけでなく、他のレポートにも応用ができます。

総カバレッジ率

GitLabで総カバレッジ率を確認できるようにしましょう。

総カバレッジ率 総カバレッジ率

GitLabはGitLab CIのジョブのログに出力される総カバレッジ率の数値を読み込むことができます。

例えば、次はNode.jsのテスティングフレームワークのJestでの出力の一部ですが、GitLabはこの内容から総カバレッジ率を読み取ります。

...

 PASS  ./sum.test.js
  ✓ adds 1 + 2 to equal 3 (1 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |     100 |      100 |     100 |     100 |                   
 sum.js   |     100 |      100 |     100 |     100 |                   
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total

...

残念ながら、dotnet test --collect:"XPlat Code Coverage"の出力には総カバレッジ率が含まれていません。HTML化したレポートには含まれていますので、grep 'Line coverage:' coverage/index.htmlとして、総カバレッジ率をジョブに出力します。(HTMLタグが含まれているのはかっこ悪いですが…)

<tr><th>Line coverage:</th><td>58.3% (7 of 12)</td></tr>

ファイル.gitlab-ci.ymltestジョブを次のように修正します。

test:
  script:
    - dotnet tool restore
    - |
      dotnet test \
        --test-adapter-path:. \
        --logger:"junit;LogFilePath=${CI_PROJECT_DIR}/artifacts/{assembly}-test-result.xml;MethodFormat=Class;FailureBodyFormat=Verbose" \
        --collect:"XPlat Code Coverage"
  after_script:
    - |
      dotnet reportgenerator \
        -reports:./**/coverage.cobertura.xml \
        -targetdir:coverage \
        -reporttypes:Html
    - grep 'Line coverage:' coverage/index.html
  coverage: '/Line coverage:.*%/'
  artifacts:
    when: always
    expose_as: coverage
    paths:
      - coverage/
    reports:
      junit:
        - ./artifacts/*test-result.xml
      cobertura:
        - ./**/coverage.cobertura.xml

coverage: '/Line coverage:.*%/'は総カバレッジ率が出力されている行を指定するためのものです。

修正が終わったらコミットしてGitLabにプッシュします。

単体テストと同じようにパイプラインからtestジョブを開きます。ジョブが完了すると、右上に総カバレッジ率が表示されます。

ジョブの総カバレッジ率 ジョブの総カバレッジ率

以上でコードカバレッジをGitLabで確認するための設定は全て終わりました。

マージリクエストでコードカバレッジを分析する

設定が終わったので、マージリクエストを作って実際にコードカバレッジを分析してみましょう。

軽減税率の導入

サンプルプロジェクトを利用している人はこのセクションの修正は終わっています。ファイルを修正する必要はありません。

まず、ブランチを作成します。

git checkout -b reduced-tax-rate

軽減税率などの導入のためにクラスを修正していきます。

ファイルConsumptionTax\消費税区分.csを追加します。

namespace ConsumptionTax
{
    public enum 消費税区分
    {
        通常,
        軽減税率,
        なし
    }
}

ファイルConsumptionTax\消費税.csを次のように修正します。

using System;
using System.ComponentModel;

namespace ConsumptionTax
{
    public class 消費税
    {
        public static int 消費税額(int 税抜金額, 消費税区分 消費税区分)
        {
            if (消費税区分 == 消費税区分.通常)
            {
                var 税額 = 税抜金額 * 0.10m;
                return (int) Math.Floor(税額);
            }

            if (消費税区分 == 消費税区分.軽減税率)
            {
                var 税額 = 税抜金額 * 0.08m;
                return (int) Math.Floor(税額);
            }

            if (消費税区分 == 消費税区分.なし) return 税抜金額;

            throw new InvalidEnumArgumentException(nameof(消費税区分), (int) 消費税区分, typeof(消費税区分));
        }
    }
}

ファイルConsumptionTax.Tests\消費税のテスト.csを次のように修正します。コードカバレッジを分析したときに面白くなるように、わざとテストパターンを不足させています。

using Xunit;

namespace ConsumptionTax.Tests
{
    public class 消費税のテスト
    {
        [Fact]
        public void 税抜100円の消費税額は10()
        {
            var actual = 消費税.消費税額(100, 消費税区分.通常);

            Assert.Equal(10, actual);
        }

        [Fact]
        public void 税抜100円の消費税額は8()
        {
            var actual = 消費税.消費税額(100, 消費税区分.軽減税率);

            Assert.Equal(8, actual);
        }
    }
}

コミットしてGitLabにプッシュ後、マージリクエストを作成してください。

このブランチのパイプライン処理が完了したら、マージリクエストからテスト結果やコードカバレッジにアクセスできるようになります。

総カバレッジ率

作成したマージリクエストの概要タブのパイプライン #...のすぐ下にあるTest coverage...の部分で、総カバレッジ率とマージリクエストでの増減がわかります。

総カバレッジ率 総カバレッジ率

ここでは総カバレッジ率が71.40%で、masterに比べ4.80%上昇しているのがわかります。

ですが、実際の開発プロジェクトでは、マージリクエストで修正するソースコードの量と全体のソースコードの量を考えると、ほぼ変化しないことが予測されます。総カバレッジ率はともかく、増減値を見てもあまり意味はないでしょう。

また、GitLabの該当プロジェクトのサイドメニューの分析->リポジトリにあるCode coverage statistics...から総カバレッジ率の推移が確認できます。こちらの推移グラフのほうが分析の役に立ちそうです。

総カバレッジ率の推移グラフ 総カバレッジ率の推移グラフ

(サンプルプロジェクトの推移グラフはデータが少なくて面白くないので、GitLabの公式ドキュメントから画像を引用しています)

コード差分でのコードカバレッジ

作成したマージリクエストの変更タブで、コードカバレッジを確認できます。(緑や赤の縦線が表示されないときはウェブブラウザーをリロードしてみてください)

マージリクエストのコード差分でのコードカバレッジ マージリクエストのコード差分でのコードカバレッジ

緑の縦線がテストで実行された行、赤の縦線がテストで実行されていない行になります。

見ての通り、消費税区分.なしと異常な消費税区分のテストケースが不足していることを発見できます。テストケースにこれらを加える必要があることがわかりました。

詳細なコードカバレッジレポート

作成したマージリクエストの概要タブの1 件の公開されたアーティファクトを表示をクリックし、続けてcoverage->coverage->index.htmlとクリックすると、詳細なHTML化されたコードカバレッジレポートにアクセスできます。

コード差分でのコードカバレッジで確認できることに加え、変更のないコードのコードカバレッジやクラスごとのカバレッジ率も確認できます。

コードカバレッジの概要 コードカバレッジの概要

クラスごとの詳細なコードカバレッジ クラスごとの詳細なコードカバレッジ

一般的に、ビジネスロジック中心のクラスはカバレッジ率が高い傾向にあります。ビジネスロジッククラスなのにもかかわらず、他よりカバレッジ率が低いクラスがあれば、クラスの詳細レポートを開いて、テストケース不足がないか分析すると良いでしょう。

最後に

コードカバレッジを利用すると、テストケースが不足しているところを見つけるのに役立ちます。GitLabで(特にマージリクエストで)コードカバレッジを確認できるようにすると大変便利です。

開発者が自身の書いたコードのテストケース不足に気がつく手助けになりますし、レビュアーや品質管理者がレポートにすぐにアクセスできるので、広域な分析がやりやすくなります。

ですが、コードカバレッジでは、不足したテストケースを全て発見できるわけではありません。

今回の分析で見つかった、消費税なしや異常な区分のテストを追加すれば、コードカバレッジ率は100%になります。しかし、実際には小数を切り捨てているかどうかのテストケースが不足しています。(例えば、15円の消費税は1円や、19円の消費税は1円のテストケースが必要)このことはコードカバレッジのレポートからはわかりません。

コードカバレッジを無闇矢鱈に信用するのはよくありませんが、品質の向上のための道具の一つと割り切れば、かなり有用です。

GitLabでコードカバレッジを分析し、品質の向上につなげていきましょう!

サンプルプロジェクトを書き換えたい方向け (おまけ)

  1. GitLabのプロジェクトの作成で、プロジェクトのインポートをクリックします
  2. Import project fromリポジトリURLをクリックします
  3. Git リポジトリ URLhttps://gitlab.com/creationline/gitlab-coverage-dotnet-sample.gitをコピペします
  4. プロジェクトを作成ボタンをクリックします

この方法でプロジェクトを作成しても、パイプラインが実行されません。パイプラインを手動で実行してください。

  1. サイドメニューのCI/CD ->パイプラインをクリックし、パイプラインページを開きます
  2. Run pipelineをクリックし、Run for branch name or tagmasterブランチを選択してRun pipelineをクリックしてパイプラインを開始します
  3. reduced-tax-rateブランチも同様にパイプラインを開始します
  4. サイドメニューからマージリクエストをクリックし、reduced-tax-rateブランチのマージリクエストをを作成します

以上で完了です。

作成したマージリクエストで、コードカバレッジを確認し、テスト不足を修正してコードカバレッジを再度確認してみてください。

Gitlab x icon svg