私は普段、gtestとlcovを使って単体テストを書いて、lcovというツールを使ってコードカバレッジを把握しています。
私の場合、すでにプロジェクトのCI環境で構築されていたものを利用しているので、一度自分で1から作ってより深く理解したいと思います。
日常的に業務で使う場合は、Github Actionsに組み込んで動かす方が楽かと思いますが、状況によっては手元で高速に動かしたいという場合がありますので、そのような場面で役立つと思います。
実行環境
毎度のことながらWindows11上のWSL2環境のUbunutu 22.04のローカルマシンで実行します。
環境構築
どなたでも再現しやすいように、そしてローカル環境を汚さないようにする目的でDockerfileを使ってコンテナ環境で実行するようにします。
Dockerfile作成
Dockerfileは次のようにします。
FROM ubuntu:22.04
RUN apt-get update \
&& apt-get -y install build-essential \
&& apt-get install -y wget \
&& rm -rf /var/lib/apt/lists/* \
&& wget https://github.com/Kitware/CMake/releases/download/v3.24.1/cmake-3.24.1-Linux-x86_64.sh \
-q -O /tmp/cmake-install.sh \
&& chmod u+x /tmp/cmake-install.sh \
&& mkdir /opt/cmake-3.24.1 \
&& /tmp/cmake-install.sh --skip-license --prefix=/opt/cmake-3.24.1 \
&& rm /tmp/cmake-install.sh \
&& ln -s /opt/cmake-3.24.1/bin/* /usr/local/bin
RUN wget https://github.com/google/googletest/archive/release-1.8.0.tar.gz \
&& tar zxvf release-1.8.0.tar.gz \
&& cd googletest-release-1.8.0/googletest/ \
&& mkdir build \
&& cd build \
&& cmake .. \
&& make \
&& make install
RUN apt-get update \
&& apt-get install -y lcov
ubuntu22.04のOSイメージを使って、Cmakeをインストールした後にgoogle testをダウンロードしてビルドしています。
最後にlcovをインストールしています。
Docker imageのbuild
上記のDockerfileが存在するディレクトリで
docker build . -t lcov
を実行して、Docker imageをlcovという名前でビルドします。
しばらく待つと、
Successfully built 2c52d5338b67
Successfully tagged lcov:latest
と表示され、lcovという名前とlatestというタグでDocker imageのビルドが完了しました。
で作成済みのimageを確認してみると次のように表示されました。
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
lcov latest 2c52d5338b67 2 minutes ago 573MB
Docker Container起動
先ほど
で確認した際に表示された を指定して します。$ docker run -it -v .:/app 2c52d5338b67
root@9161e42cde91:/#
無事、Docker Containerのbashが起動されました。
また、
オプションを使用して、カレントディレクトリをDocker container内の/appディレクトリにマウントしています。テスト対象のコード
今回はC++で計算機クラスを定義してみました。
calculator.hppを次のようにして、
#ifndef CALCULATOR_HPP
#define CALCULATOR_HPP
#include <stdexcept>
class Calculator {
public:
int add(int a, int b) ;
int subtract(int a, int b) ;
int multiply(int a, int b) ;
int divide(int a, int b);
};
#endif
calculator.cppを次のようにしました。
#include "calculator.hpp"
int Calculator::add(int a, int b) {
return a + b;
}
int Calculator::subtract(int a, int b) {
return a - b;
}
int Calculator::multiply(int a, int b) {
return a * b;
}
int Calculator::divide(int a, int b) {
if (b == 0) {
throw std::invalid_argument("Devided by zero");
}
return a / b;
}
テストコードの実装
test_calculator.cppというファイルを作成して次のように実装しました。
#include <gtest/gtest.h>
#include "calculator.hpp"
class CalculatorTest : public ::testing::Test {
protected:
Calculator calculator;
};
TEST_F(CalculatorTest, Addition) {
EXPECT_EQ(calculator.add(2, 3), 5);
}
とりあえず、add関数のテストコードを書いてカバレッジが変わる様子を見たいと思います。
ビルドと実行(カバレッジ情報取得)
gtestの実行
先ほどマウントした/appディレクトリに移動して次のようにg++を使ってビルドします。
root@cb433f4154df:/app# g++ calculator.cpp test_calculator.cpp -o test -lgtest_main -lgtest
各オプションの意味は次の通り。
: 出力される実行ファイルの名前を指定。今回はtestを指定。
:Google Testフレームワークのmain関数をリンクします。これは、Google Testのエントリーポイントを提供します。
:Google Testフレームワークライブラリをリンクします。
ひとまず、これらのオプションでgtestが実行できます。
次のように出力されたtestを実行するとgtestのmain関数が実行されてテスト結果が出力されます。
root@cb433f4154df:/app# ./test
Running main() from gtest_main.cc
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from CalculatorTest
[ RUN ] CalculatorTest.Addition
[ OK ] CalculatorTest.Addition (0 ms)
[----------] 1 test from CalculatorTest (0 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (0 ms total)
[ PASSED ] 1 test.
プロファイリングデータの取得
さて、今回はlcovを使って単体テストのコードカバレッジを取得することが目的なので、さらにビルド時のオプションを追加します。
追加するオプションは次の通り。
: コードカバレッジを測定するために使用されます。
: どの部分のコードが実行されたかを追跡することができます。
これらのオプションを追加して次のようにビルドしてみます。
root@cb433f4154df:/app# g++ calculator.cpp test_calculator.cpp -o test -lgtest_main -lgtest -fprofile-arcs -ftest-coverage
するとカレントディレクトリに、次の2つのファイルが生成されました。
test-calculator.gcno
test-test_calculator.gcno
その後、testを実行すると次の2つのファイルが生成されました。
test-calculator.gcda
test-test_calculator.gcda
gcdaとgcnoファイルのそれぞれの内容は次の通りです。
gcda: [概要] 実行時に収集されたカバレッジ情報を含むファイル。
[内容] プログラムの実行中に、どのブロックや行が実行されたかの情報が記録される。
[生成タイミング] プログラム実行中。
gcno: [概要] コンパイル時に生成されるカバレッジ情報を含むファイル。
[内容] コードの各行やブロックがどのように計測されるかの情報が含まれる。
[生成タイミング] コードをコンパイルするタイミング。
確かに、内容の通り、それぞれのファイルがコンパイル時と実行時にそれぞれ生成されました。
ここまでで、カバレッジ情報が取得できたので、あとは可視化すればいいだけです。
lcovによる可視化
可視化データの作成
次のコマンドを実行して、カバレッジデータを.infoファイルに出力します。
lcov -c -d . -o coverage.info
: カバレッジデータを収集することを指定する。captureの略。
: カバレッジデータを収集するディレクトリを指定する。今回はカレントディレクトリ。
: 収集されたカバレッジデータを保存する出力ファイルを指定。今回はcoverage.info。
.infoのファイルはバイナリではなく、中身はテキストとして表示できます。
このデータを使ってhtmlを作成して、Webブラウザで表示します。
genhtml -o ./lcov-out coverage.info
このコマンドによって、./lcov-outディレクトリにhtml形式のデータを出力できました。

Webブラウザで表示
ここまで来て、WSL上でやっていることを後悔しました。。 Webブラウザですぐに表示できない。
VS codeを使っているので、何かhtml表示のExtentionを利用すれば便利かもしれません。
ひとまず、zipコマンドをインストールして、lcov-outディレクトリをzip化して
ホスト側のWindowsマシンにダウンロードして、Webブラウザで表示しました。
一応、zipのインストールとzip化のコマンドは次の通りです。
# apt install zip
# zip -r archive.zip ./lcov-out
さて、Webブラウザでindex.htmlを表示すると次のようになりました。

カバレッジが低いのは想定通りですが、何かおかしい。。
app配下のcalculatorがテスト対象なのに、色々と他のものが混ざっています。
appの中を見るとtestコードのカバレッジもでています。。

ひとまず、calculator.cppの中は意図通り、add関数がテストされたことになっていることが分かります。

フィルタリングの設定
上記のようにテストコードまで含めた形でカバレッジが出力されるのは、
デフォルトではテストコードも含めた全体のカバレッジデータを取得するからのようです。
さきほど実行した次のコマンドだけだとテストコードも含んでカバレッジデータを収集してしまいます。
#lcov -c -d . -o coverage.info
なので、
というオプションを使って次のように がつくファイルを除外してみました。#lcov --remove coverage.info '*test_*' -o coverage_filtered.info
Reading tracefile coverage.info
Removing /app/test_calculator.cpp
Deleted 1 files
Writing data to coverage_filtered.info
Summary coverage rate:
lines......: 41.0% (50 of 122 lines)
functions..: 38.6% (22 of 57 functions)
branches...: no data found
出力を見ると、意図通り、test_calculator.cppが除外されたようです。
しかし、さきほどのhtmlの結果からすると
テストコード以外にも標準ライブラリなど自分が実装していないコードもカバレッジデータに含まれているようでした。
なので、カバレッジデータ収集対象のコードを指定する方が良さそうです。
ということで、次のように
オプションを使って特定のコードのみのカバレッジデータを収集して出力しました。# lcov --extract coverage.info '*/calculator.cpp' -o coverage_target.info
Reading tracefile coverage.info
Extracting /app/calculator.cpp
Extracted 1 files
Writing data to coverage_target.info
Summary coverage rate:
lines......: 20.0% (2 of 10 lines)
functions..: 25.0% (1 of 4 functions)
branches...: no data found
出力されたcoverage_target.infoからgenhtmlすると、次のように無事、対象のコードのカバレッジデータを可視化できました。

おまけ
ここまでで、十分ローカルでgtestを実行してlcovによる可視化ができたので、ひとまずは終わってもいいのですが、一応カバレッジを上げてみます。
カバレッジを上げてみる
次のようにテストコードを書いてカバレッジを上げます。
#include <gtest/gtest.h>
#include "calculator.hpp"
class CalculatorTest : public ::testing::Test {
protected:
Calculator calculator;
};
TEST_F(CalculatorTest, Addition) {
EXPECT_EQ(calculator.add(2, 3), 5);
}
TEST_F(CalculatorTest, Subtraction) {
EXPECT_EQ(calculator.subtract(5, 3), 2);
}
TEST_F(CalculatorTest, Multiplication) {
EXPECT_EQ(calculator.multiply(2, 3), 6);
}
TEST_F(CalculatorTest, Division) {
EXPECT_EQ(calculator.divide(6, 3), 2);
}
TEST_F(CalculatorTest, DivisionByZero) {
EXPECT_THROW(calculator.divide(6, 0), std::invalid_argument);
}
ブランチカバレッジを表示する
また、さきほどの結果だとLine Coverageだけ表示されていたので、Branch Coverageも表示するようにします。
lcovでカバレッジデータを収集する際のオプションに
を追加します。lcov -c -d . -o coverage.info --rc lcov_branch_coverage=1
そしてextractするときも同様にオプションを追加します。
lcov --extract coverage.info '*/calculator.cpp' -o coverage_target.info --rc lcov_branch_coverage=1
すると、標準出力からでもbranch coverageが確認できました。
Summary coverage rate:
lines……: 100.0% (10 of 10 lines)
functions..: 100.0% (4 of 4 functions)
branches…: 75.0% (3 of 4 branches)
この時点でさきほどよりカバレッジが上がったことが分かりますが、htmlでも確認しておきます。
genhtml実行時は
というオプションを付けます。genhtml --branch-coverage -o ./lcov-out-new coverage_target.info
すると、無事Branchesという行が追加されました。

コードの中身は次のような感じ。

0で割った場合の例外を投げる部分も分岐として扱われていますね。
まとめ
今回はgtestで単体テストした際のコードカバレッジをlcovで可視化することをWSL上のDockerコンテナ内で行ってみました。
普段、開発をしているとこの辺りの作業は自動化されているので意識しないですが、
手動で手順を辿ってみると理解が深まるものですね。
今回使用したコマンドもシェルスクリプトとして書いたり、コードのbuildはCmakeを使うと自動化できてミスもなくなると思います。
以上、皆さんの参考になれば嬉しいです。
最後までお読み頂き、ありがとうございました。
コメント