🧪

【Rust】cargo-llvm-covでコードカバレッジを取得する

2024/05/06に公開

はじめに

cargo-llvm-covは、Rustでテストされたコードの割合「カバレッジ」を計測するツールです。

テストされるコードが増えると潜在的な不具合を発見しやすくなるため、
カバレッジの高さは、コード品質の指標にすることがあります。

当記事ではcargo-llvm-covでテストを実行し、カバレッジを確認する手順をご紹介します。

事前準備

cargo-llvm-covllvm-tools-previewをインストールします。

% cargo install cargo-llvm-cov
% rustup component add llvm-tools-preview

「LLVM」は、コンパイラ基盤のことです。
参考:LLVMとは | DevelopersIO

テスト対象のコード

前回の記事「【Rust】モックライブラリMockitoで非同期のテストコードを書く」で作成した、
JSONPlaceholderからToDoデータを取得する処理をテスト対象とします。

APIを実行しToDoを取得するfetch_todo_api()と、
ToDoを出力用文字列で返すtodo_details()という関数があります。

コード全体は、以下を展開して確認ください。

テスト対象コード全体
Cargo.toml
[dependencies]
tokio = { version = "1.37", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
mockito = "1.4"
main.rs
use reqwest::Error;
use serde::Deserialize;

#[derive(Deserialize)]
struct Todo {
    #[serde(rename = "userId")]
    user_id: u32,
    id: u32,
    title: String,
    completed: bool,
}

async fn fetch_todo_api(url: &str) -> Result<Todo, Error> {
    let response = reqwest::get(url).await?;
    response.json::<Todo>().await
}

fn todo_details(todo: &Todo) -> String {
    format!(
        "Todo:\nUserId: {}\nId: {}\nTitle: {}\nCompleted: {}",
        todo.user_id, todo.id, todo.title, todo.completed
    )
}

#[tokio::main]
async fn main() {
    if let Some(todo_id) = std::env::args().nth(1) {
        let base_url = "https://jsonplaceholder.typicode.com/todos/";
        let url = format!("{}{}", base_url, todo_id);
        match fetch_todo_api(&url).await {
            Ok(todo) => println!("{}", todo_details(&todo)),
            Err(err) => eprintln!("Error: {}", err),
        }
    } else {
        eprintln!("Error: Todo ID not provided");
    }
}

テストコードの作成

テストコードは前回作成したtest_fetch_todo_api()と、
新たに作成したtest_todo_details()を加えた2つにしました。

テストコード全体
#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_fetch_todo_api() {
        let mut server = mockito::Server::new_async().await;
        let path = "/todos/1";
        let json_body = r#"{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}"#;

        let mock = server.mock("GET", path)
            .with_status(200)
            .with_header("content-type", "application/json")
            .with_body(json_body)
            .create_async().await;

        let url = server.url() + path;
        let todo: Todo = fetch_todo_api(&url).await.unwrap();

        assert_eq!(todo.user_id, 1);
        assert_eq!(todo.id, 1);
        assert_eq!(todo.title, "delectus aut autem");
        assert!(!todo.completed);

        mock.assert_async().await;
    }

    #[test]
    fn test_todo_details() {
        let todo = Todo {
            user_id: 1,
            id: 2,
            title: "quis ut nam facilis et officia qui".to_string(),
            completed: false,
        };

        let expected = "Todo:\nUserId: 1\nId: 2\nTitle: quis ut nam facilis et officia qui\nCompleted: false";

        assert_eq!(todo_details(&todo), expected);
    }
}

llvm-covを実行

以下コマンドで実行します。

% cargo llvm-cov

標準出力(ターミナル)に、結果が表示されました。

running 2 tests
test tests::test_todo_details ... ok
test tests::test_fetch_todo_api ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s

Filename        Regions    Missed Regions     Cover   Functions  Missed Functions  Executed       Lines      Missed Lines     Cover    Branches   Missed Branches     Cover
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
src/main.rs          59                25    57.63%          11                 2    81.82%          55                11    80.00%           0                 0         -
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TOTAL                59                25    57.63%          11                 2    81.82%          55                11    80.00%           0                 0         -

4種類のカバレッジが出力されました🎊

  • Function Coverage:テストされた関数の数で計測したテスト網羅率
  • Line Coverage:テストされた行の数で計測したテスト網羅率
  • Region Coverage:?||演算子による分岐を加味したテスト網羅率
  • Branch coverage:if文などの条件分岐を加味したテスト網羅率

HTMLレポートを出力

標準出力では結果が少し見辛いですが、
--openを付けて実行すると、見やすいHTML形式でレポートを作成できます♪

% cargo llvm-cov --open

HTMLファイルはデフォルトで、target/llvm-cov/html/index.htmlに保存されます。

今回の結果は、ファンクションカバレッジで約82%、ラインカバレッジで80%でした。
main関数内の処理をリファクタリングで関数化し、
テストを増やすことでカバレッジはもっと上げられそうです。

VSCodeでカバレッジを確認

Visual Studio Codeの拡張機能「Coverage Gutters」を利用すると、
テストされた行と、テストされてない行がエディタ上で色分け表示されるので便利です!

  1. VSCodeにCoverage Guttersをインストールします。
  2. 以下のようにオプションを付けて、LCOVという形式でテスト結果を出力します。
% cargo llvm-cov --lcov --output-path lcov.info
  1. VSCodeの下部にラインカバレッジが表示されます。
  2. コードの左端がテストされた行は緑、未テストの行は赤で表示されます。

おわりに

Rustのカバレッジ取得ツールとしては「cargo-llvm-cov」の他に、
cargo-tarpaulin」や「grcov」があり、ダウンロード数は拮抗していました。

今回は直近のダウンロード数が多いcargo-llvm-covを選んでみましたが、
ブランチカバレッジを除けば、簡単にカバレッジを取得でき使いやすいツールでした。

実行オプションやサブコマンドがいろいろあるので、また試してみたいと思います。

https://crates.io/crates/cargo-llvm-cov/

コラボスタイル Developers

Discussion