ys memos
Blog

Rustでcoverage計測する ~3. カバレッジ計測の範囲無効~


rust

2025/06/08



カバレッジの計測とその可視化で、一定の効果があることは疑いがないが、カバレッジを可視化すると、大きな問題点なのではと思うことがあった。

それが、「Testコードがカバー率に含まれる」ことである。

これを何故問題と思ったかというと、カバレッジを測る目的は「実装コードのテストの網羅性を確認する」ことであり、Testコードはその目的に含まれないからである。さらに、基本的にTestコードは100%のコードを通過するのが基本である。

例えば、特定のファイルに実装とTestが含まれているとして、

  • コード20行のうち10行 (50%)がカバー
  • Testコードが20行あり100%がカバー

されている場合、コードのカバレッジは、(10+20)/(20+20)=75%となり、本来の計測対象のカバレッジよりも、ファイルのカバレッジが高く見えてしまう。

そこで、カバレッジ計測の対象から特定の範囲を除外する方法を紹介する。

以下、カバレッジは Line Coverageを指すものとする。


まず、カバー率を高めたTestから、一番下のものを取り除いた。

src/main.rs
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の先頭に以下を追加する。

src/main.rs
#![feature(coverage_attribute)]

コード全体をこのように書き換える。

変更部分は、カバレッジから除外したい関数に#[coverage(off)]を付けている。

src/main.rs
#![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機能と比べて利用のリスクは低いと推測される。)



関連タグを探す