2025/06/08
目次
はじめに
カバレッジの計測とその可視化で、一定の効果があることは疑いがないが、カバレッジを可視化すると、大きな問題点なのではと思うことがあった。
それが、「Testコードがカバー率に含まれる」ことである。
これを何故問題と思ったかというと、カバレッジを測る目的は「実装コードのテストの網羅性を確認する」ことであり、Testコードはその目的に含まれないからである。さらに、基本的にTestコードは100%のコードを通過するのが基本である。
例えば、特定のファイルに実装とTestが含まれているとして、
- コード20行のうち10行 (50%)がカバー
- Testコードが20行あり100%がカバー
されている場合、コードのカバレッジは、(10+20)/(20+20)=75%
となり、本来の計測対象のカバレッジよりも、ファイルのカバレッジが高く見えてしまう。
そこで、カバレッジ計測の対象から特定の範囲を除外する方法を紹介する。
以下、カバレッジは Line Coverageを指すものとする。
サンプルコード更新
まず、カバー率を高めたTestから、一番下のものを取り除いた。
type SampleResult<T> = Result<T, Box<dyn std::error::Error>>;
fn main() -> SampleResult<()> {
let args = std::env::args().collect::<Vec<String>>();
let message = greet(args)?;
println!("{message}");
Ok(())
}
fn greet(args: Vec<String>) -> SampleResult<String> {
let name = args.get(1).ok_or("")?;
let age = match args.get(2) {
Some(value) => Some(value.parse::<u32>()?),
None => None,
};
Ok(match age {
Some(age) => format!("Hello, {name}! You are {age} years old."),
None => format!("Hello, {name}!"),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_main() {
assert!(main().is_err());
}
#[test]
fn greet_empty() {
let args = vec![];
assert!(greet(args).is_err());
}
#[test]
fn greet_with_name() -> SampleResult<()> {
let args = vec!["program".to_string(), "Alice".to_string()];
let message = greet(args)?;
assert_eq!(message, "Hello, Alice!");
Ok(())
}
}
実際の結果は下に載せるが、このコードのLineCoverageは85.71% (24/28)
となった。可視化すると、この算出には、Testのコードも含まれていて、ロジックとTestを分けて式にしてみると(11+13)/(15+13)=24/28
となっている。
ここで計測されてほしいのは、ロジックの部分であり、 11/15=73.33%
のカバレッジである。
使うもの
#[coverage(off)]
を使う
これは、 nightly
であるため、注意が必要。
使用準備
main.rs
の先頭に以下を追加する。
#![feature(coverage_attribute)]
Testコードの除外
コード全体をこのように書き換える。
変更部分は、カバレッジから除外したい関数に#[coverage(off)]
を付けている。
#![feature(coverage_attribute)]
type SampleResult<T> = Result<T, Box<dyn std::error::Error>>;
fn main() -> SampleResult<()> {
let args = std::env::args().collect::<Vec<String>>();
let message = greet(args)?;
println!("{message}");
Ok(())
}
fn greet(args: Vec<String>) -> SampleResult<String> {
let name = args.get(1).ok_or("")?;
let age = match args.get(2) {
Some(value) => Some(value.parse::<u32>()?),
None => None,
};
Ok(match age {
Some(age) => format!("Hello, {name}! You are {age} years old."),
None => format!("Hello, {name}!"),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[coverage(off)]
fn test_main() {
assert!(main().is_err());
}
#[test]
#[coverage(off)]
fn greet_empty() {
let args = vec![];
assert!(greet(args).is_err());
}
#[test]
#[coverage(off)]
fn greet_with_name() -> SampleResult<()> {
let args = vec!["program".to_string(), "Alice".to_string()];
let message = greet(args)?;
assert_eq!(message, "Hello, Alice!");
Ok(())
}
}
計測(設定後)
#[coverage(off)]
を使うので、nightly
である必要があり、以下のように+nightly
を付けて実行する。
カバー計測の範囲からTestコードが除外され、カバレッジを期待通りに下げることができた。
$ cargo +nightly llvm-cov
info: cargo-llvm-cov currently setting cfg(coverage) and cfg(coverage_nightly); you can opt-out it by passing --no-cfg-coverage and --no-cfg-coverage-nightly
Compiling coverage v0.1.0 (/<path>/<to>/<project>/coverage)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.13s
Running unittests src/main.rs (target/llvm-cov-target/debug/deps/coverage-3b5b0267fefa37f7)
running 3 tests
test tests::greet_empty ... ok
test tests::greet_with_name ... ok
test tests::test_main ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Filename Regions Missed Regions Cover Functions Missed Functions Executed Lines Missed Lines Cover Branches Missed Branches Cover
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
/<path>/<to>/<project>/coverage/src/main.rs 30 10 66.67% 2 0 100.00% 15 4 73.33% 0 0 -
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TOTAL 30 10 66.67% 2 0 100.00% 15 4 73.33% 0 0 -
計測(設定前)
参考として、設定前のカバレッジがこちら。
$ cargo llvm-cov
info: cargo-llvm-cov currently setting cfg(coverage); you can opt-out it by passing --no-cfg-coverage
Compiling coverage v0.1.0 (/<path>/<to>/<project>/coverage)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.23s
Running unittests src/main.rs (target/llvm-cov-target/debug/deps/coverage-90ad100d3a119901)
running 3 tests
test tests::greet_empty ... ok
test tests::test_main ... ok
test tests::greet_with_name ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Filename Regions Missed Regions Cover Functions Missed Functions Executed Lines Missed Lines Cover Branches Missed Branches Cover
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
/<path>/<to>/<project>/coverage/src/main.rs 30 6 80.00% 5 0 100.00% 28 4 85.71% 0 0 -
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TOTAL 30 6 80.00% 5 0 100.00% 28 4 85.71% 0 0 -
おわりに
サンプルコードではカバレッジへの影響は大きくはなかったが、実際に多くのパターンのTestがある場合、よりカバレッジへの影響が大きくなるため、カバレッジ計測の目的にあっていれば、Testコードを除外することは有効と思われる。
無理してカバレッジを100%にすることだけが全てではないと考えられるが、低い状態をキープしてしまうと、Testの網羅性に対する意識が薄れてしまう可能性があるため、これらの工夫はいつかのコード品質に寄与するかもしれない。
ただし、2025/06/08 現時点では、 #[coverage(off)]
は、nightly
のみで使用可能であるため、導入の際はこれもトレードオフになるだろう。(ただし、llvm-cov実行での話なので、一般的なnightly機能と比べて利用のリスクは低いと推測される。)