カバレッジテスト

2022年11月27日

DevOpsでは、ソフトウェアの品質をどのようにして保っていくかというテスト戦略を考えることも重要となってくる。
V字工程の中で、各ソフトウェアコンポーネント単体で行うテストの一つとしてカバレッジテストがある。
テストを行う際に、対象範囲(コンポーネント)に対してどの程度コードを網羅したかの指標となるのがカバレッジである。
本ページでは、サンプルコードを用いて実際にカバレッジを取得し、カバレッジの種類についての理解を深めていく。

使用するツール

今回使用するツールは、以前の記事でも少し触れた「Bazel coverage」を使用する。
Bazel Coverageは、内部でgcovを使用しているのでgcovの仕様でCoverageを取ることになる。
後に詳細を記載するが、gcovを用いて出力されるのが、Lineカバレッジ、Functionカバレッジ、Branchカバレッジになっていて、今回説明する一部のカバレッジしかサポートしていない。
そのため、sampleコードを用いての説明は、一部のみとなる。

また、単体テストのフレームワークとしてGoogle Test(正式:Google C++ Test Framework)を用いる。
Google Testの詳細に関しては、別の機会に記載できればと思う。

sampleコード

以下の非常に簡単なテスト対象コードで試行する。

#include <iostream>
using namespace std;

int function(int e, int f){
	int h;
	if ( e > 10 ){
		h = e;
	}else{
		h = e*e;
	}
	if ( f > 10 ){
		h = f;
	}
	return h;
}
※その他のファイルの構成
/test_dir
|--WORKSPACE
|--main
|  |--BUILD
|  |--sample.cc
|  |--sample.hpp
|  |--unit_test.cc
workspace(name = "coverage_test")

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")


http_archive(
  name = "com_google_googletest",
  urls = ["https://github.com/google/googletest/archive/609281088cfefc76f9d0ce82e1ff6c30cc3591e5.zip"],
  strip_prefix = "googletest-609281088cfefc76f9d0ce82e1ff6c30cc3591e5",
)

http_archive(
    name = "eigen",
    urls = ["https://bitbucket.org/eigen/eigen/get/3.3.7.tar.gz"],
    sha256 = "7e84ef87a07702b54ab3306e77cea474f56a40afa1c0ab245bb11725d006d0da",
    build_file = "//autonomy/external_deps:BUILD.eigen",
)
cc_library(
    name = 'sample',
    hdrs = ['sample.hpp',
           ],
    srcs = ['sample.cc',
           ],
    linkstatic = True,
    visibility = ["//visibility:public"],
)

cc_test(
    name = 'hello_test',
    srcs = ['unit_test.cc'],
    deps = ['sample',
            '@com_google_googletest//:gtest_main']
)
int function(int e, int f);

unit_test.ccは以下の章でそれぞれ示す。

このコード自体特に意味のあるコードではない。あくまでカバレッジを理解するために用いている。
フローで表すと以下のようになる。

start
start
e > 10
e > 10
h = e
h = e
True
True
h = e*e
h = e*e
False
False
f >10
f >10
h = f
h = f
True
True
end
end
False
False
条件分岐(1)
条件分岐(1)
条件分岐(2)
条件分岐(2)
命令(1)
命令(1)
命令(2)
命令(2)
命令(3)
命令(3)
Viewer does not support full SVG 1.1

ではこのテスト対象コードを用いて、カバレッジの種類について説明していく。

C0カバレッジ(Statementカバレッジ)

C0カバレッジは、別名Statementカバレッジや命令網羅とも言われる。
上記コードの例だと、命令が□で囲まれた箇所で、"h=e"、"h=e*e"、"h=f"が該当する。
これら3つの命令をすべて網羅するテストケースを作成すれば、C0カバレッジ100%となる。
まずは、条件分岐(1)がTrue、条件分岐(2)もTrueの場合を見てみる。
Google Testの"EXPECT_EQ"マクロを用いて、対象コードのテストを実行する。
テストコードとしては、以下のようになる。(eに11を,fに11を代入して、return hの期待値として11を指定する。)

#include "sample.hpp"
#include <gtest/gtest.h>

#include <cstdint>

// Demonstrate some basic assertions.
TEST(HelloTest, BasicAssertions) {
  EXPECT_EQ(11, function(11, 11));
}

このテストを実行すると、以下のような結果となる。(実行コマンドは、以前の記事参照)

C0カバレッジ 結果1

Bazel coverageでは、StatementカバレッジとしてLines Coverageを参照できる。
1行1行実行されたかどうかを判定する。
今回のテストケースでは、命令(2)が実行されていないためLines Coverageが100%ではない。
そこで、テストケースに追加で命令(2)を実行されるケースを追加する。

#include "sample.hpp"
#include <gtest/gtest.h>

#include <cstdint>

// Demonstrate some basic assertions.
TEST(HelloTest, BasicAssertions) {
  EXPECT_EQ(11, function(11, 11));
  EXPECT_EQ(11, function(9, 11));
}

するとすべての命令分が網羅され、Lines CoverageすなわちC0が100%となる。

C0カバレッジ 結果2

C1カバレッジ(Branchカバレッジ)

C1カバレッジは、別名Branchカバレッジ、分岐網羅とも言われる。
全ての判定条件(条件分岐)の真偽が少なくとも一回実行されているかどうかを確認する。
今回のSampleコードで見てみると、条件分岐(1)のTrue,False、条件分岐(2)のTrue,Falseのすべてを網羅すればC1カバレッジが100%となる。
そのため、C1カバレッジ100%とするテストケースは以下のようになる。

#include "sample.hpp"
#include <gtest/gtest.h>

#include <cstdint>

// Demonstrate some basic assertions.
TEST(HelloTest, BasicAssertions) {
  EXPECT_EQ(11, function(11, 11));
  EXPECT_EQ(81, function(9, 9));
}

実行結果は以下のようになる。

C1カバレッジ 結果1

C2カバレッジ(Conditionカバレッジ)

C2カバレッジは、別名Conditionカバレッジ、条件網羅とも言われる。
全ての判定条件の真偽が少なくとも一回実行されているかどうかを確認する。
Bazel coverage(gcov)では、C2カバレッジは取得できないそうだ。
ただ、イメージを持つために以下のsampleコードを考える。

#include <iostream>
using namespace std;

int function(int e, int f, int g){
	int h;
	if ( e > 10 || f > 10 ){
		h = e+f;
	}else{
		h = e*f;
	}
	if ( g > 10 ){
		h = g;
	}
	return h;
}

すべての判定条件の真偽を網羅するためには、

(1)e >10:true、f > 10:true、g > 10:false(例:eに11、fに11、gに9を代入してreturn hが22になるケース)
(2) e >10:false、f > 10:false、g > 10:true(例:eに9、fに9、gに11を代入してreturn hが11になるケース)

の二つのテストケースを実行すればよい。
この二つのケースの場合、結果としてはC0とC1は100%となるが、以下の二つのケースの場合はC2が100%となるが、C0とC1が100%とはならない。

(1)e >10:true、f > 10:false、g > 10:true(例:eに11、fに9、gに11を代入してreturn hが11になるケース)
(2) e >10:false、f > 10:true、g > 10:false(例:eに9、fに11、gに9を代入してreturn hが20になるケース)

このように、必ずしもカバレッジレベル上位が下位をカバーしているわけではないことに注意が必要。
(※C1が100%であれば、C0は100%である。)

MC/DCカバレッジ

MC/DCカバレッジは、modified condition/decision coverageの略で、改良条件判定網羅とも呼ばれる。
ISO26262 Part6-8に記載があるように、自動運転などの高い安全性が求められる要素(ASIL-D)に対してはこのカバレッジ基準を満たすことが要求される。
条件としては以下の3つある。

(1)全ての判定条件の真偽が少なくとも一回実行される(C1)
(2)全ての条件式の真偽が少なくとも一回実行される(C2)
(3)全ての条件式が、単独で全体の判定条件の結果を左右する

全ての条件の組み合わせをテストすれば一番網羅度が高いが、条件式が増えるに従いテストケースが指数関数的に増えてしまう。そこで必要なのがMC/DCカバレッジの(3)の条件だ。
MC/DCカバレッジに必要なテストケースを導出するには、真理値表も用いるとわかりやすい。

一番簡単な例だと、(A || B)の論理和が挙げられる。
真理値表は以下の通り。

No.ABResult
1TTT
2TFT
3FTT
4FFF
論理和の真理値表

条件AがTの時、Bの結果にかかわらずResultがTなので、No.1,2のテストケースはどちらかがあればよい。
条件AがFの時、Bの結果によりResult結果を左右するので、No.3,4のテストケースはどちらも必要である。
よって、(A || B)のMC/DCを満たすテストケースは(No,1or2)とNo,3、No4の3つとなる。

次に(A || (B && C))を考えてみる。

No.ABCResult
1TTTT
2TTFT
3TFTT
4TFFT
5FTTT
6FTFF
7FFTF
8FFFF
(A || (B && C))の真理値表

これも同様の考え方でテストケースをまとめると以下のようになる。

NoABCResult
1~4TX(Don’t care)XT
5FTTT
6FTFF
7,8FFXF
テストケースをまとめた真理値表

この4通りのテストケースから、各条件が判定の出力に独立して影響することを示すテストケースのペアは次の通りとなる。

(1)A→No.1~4のいずれか、No.6
(2)B→No.5、No.7またはNo.8
(3)C→No.5、No.6

よってMC/DCを満たすために必要なテストケースは、No,1~4のいずれか、No.5、No.6、No.7またはNo.8の4つとなる。

参考

Google C++ Testing Framework

Test

Posted by okuribito