cargo llvm-cov로 Rust 코드 커버리지 측정하기

cargo llvm-cov로 Rust 코드 커버리지 측정하기

테스트를 열심히 작성했는데 과연 우리 코드가 어디까지 테스트되고 있는 걸까요? “이 모듈은 테스트를 꽤 많이 짰으니까 괜찮겠지”라고 막연하게 생각하다가 정작 핵심 에러 처리 분기는 한 번도 실행된 적이 없었다는 걸 나중에 발견하는 경우가 종종 있습니다 😅

코드 커버리지는 이런 사각지대를 눈에 보이게 만들어주는 도구입니다. Rust에서는 cargo-llvm-cov가 이 역할을 맡고 있는데 LLVM의 소스 기반 커버리지 기능을 활용해서 정확한 측정 결과를 제공해요. 이번 글에서는 설치부터 기본 사용법, 여러 리포트 형식, CI 연동까지 차근차근 알아보겠습니다.

코드 커버리지가 뭔가요?

코드 커버리지(Code Coverage)는 테스트를 실행했을 때 소스 코드의 어느 부분이 실제로 실행되었는지를 수치로 보여주는 지표입니다. 보통 퍼센트로 표현하는데, “라인 커버리지 80%“라고 하면 전체 코드의 80%가 테스트 중에 최소 한 번은 실행되었다는 뜻이에요.

커버리지에는 몇 가지 종류가 있습니다. 라인 커버리지는 말 그대로 각 줄이 실행되었는지를 봅니다. 브랜치 커버리지는 if/elsematch 같은 분기가 모두 실행되었는지를 확인하고요. 리전(Region) 커버리지는 코드의 논리적 영역 단위로 실행 여부를 추적합니다.

그러면 커버리지를 왜 측정해야 할까요? 가장 큰 이유는 테스트의 빈틈을 찾기 위해서입니다. 함수를 하나 작성하면서 정상 케이스만 테스트하고 에러 케이스를 빠뜨리는 건 흔한 일이거든요. 커버리지 리포트를 보면 어떤 분기가 테스트되지 않았는지 한눈에 파악할 수 있습니다.

설치하기

cargo-llvm-cov를 사용하려면 두 가지를 설치해야 합니다. 먼저 LLVM 도구 컴포넌트를 추가하고, 그다음 cargo 서브커맨드를 설치합니다.

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

llvm-tools-preview는 Rust 컴파일러가 커버리지 데이터를 생성하는 데 필요한 LLVM 도구들을 포함하고 있습니다. 이게 없으면 cargo-llvm-cov가 동작하지 않아요.

설치가 끝나면 버전을 확인해볼 수 있습니다.

cargo llvm-cov --version

cargo-binstall을 사용하고 있다면 컴파일 시간 없이 더 빠르게 설치할 수도 있어요.

cargo binstall cargo-llvm-cov

기본 사용법

사용법은 아주 간단합니다. 프로젝트 디렉토리에서 이 명령어 하나면 됩니다.

cargo llvm-cov

이 명령어는 내부적으로 테스트를 컴파일하고 실행하면서 커버리지 데이터를 수집합니다. 실행이 끝나면 텍스트 형태로 커버리지 요약을 보여줘요.

결과
Filename                      Regions    Missed Regions     Cover   Functions  Missed Functions  Executed       Lines      Missed Lines     Cover    Branches   Missed Branches     Cover
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
src/lib.rs                         12                 2    83.33%           4                 1    75.00%          40                 8    80.00%           0                 0         -
src/parser.rs                      25                 5    80.00%           8                 2    75.00%          95                18    81.05%           0                 0         -
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TOTAL                              37                 7    81.08%          12                 3    75.00%         135                26    80.74%           0                 0         -

파일별로 리전 커버리지, 함수 커버리지, 라인 커버리지를 보여줍니다. 어느 파일의 커버리지가 낮은지 바로 알 수 있죠.

참고로 cargo llvm-covcargo test를 대체하는 게 아니라 감싸는 wrapper입니다. 테스트 자체는 동일하게 실행되고, 거기에 커버리지 측정만 얹는 거예요. 그래서 cargo test에 넘길 수 있는 옵션 대부분을 그대로 사용할 수 있습니다.

리포트 형식

텍스트 요약만으로는 구체적으로 어떤 코드가 커버되지 않았는지 파악하기 어렵습니다. cargo-llvm-cov는 여러 리포트 형식을 지원하는데 상황에 따라 골라서 쓸 수 있어요.

기본 텍스트 리포트는 따로 옵션을 주지 않아도 나오는 터미널 출력입니다. 빠르게 전체 현황을 파악할 때 유용하죠.

HTML 리포트는 가장 직관적인 형식입니다. 소스 코드의 각 줄이 실행되었는지 색상으로 표시해주거든요.

cargo llvm-cov --html

이 명령어를 실행하면 target/llvm-cov/html/ 디렉토리에 HTML 파일이 생성됩니다. 브라우저로 열면 파일별 커버리지를 클릭해서 들어갈 수 있고, 각 줄의 실행 횟수까지 확인할 수 있어요. 초록색은 실행된 줄, 빨간색은 실행되지 않은 줄을 나타냅니다.

생성된 리포트를 바로 브라우저에서 열고 싶다면 --open 플래그를 추가하면 됩니다.

cargo llvm-cov --html --open

lcov 형식은 CI 환경에서 외부 서비스에 커버리지를 업로드할 때 주로 사용합니다.

cargo llvm-cov --lcov --output-path lcov.info

이렇게 생성된 lcov.info 파일을 Codecov나 Coveralls 같은 서비스에 업로드하면 PR마다 커버리지 변화를 추적할 수 있습니다.

JSON 형식도 지원합니다. 프로그래밍적으로 커버리지 데이터를 처리하고 싶을 때 쓸 수 있어요.

cargo llvm-cov --json --output-path coverage.json

Cobertura XML 형식도 있는데, GitLab CI처럼 Cobertura 형식을 기대하는 환경에서 유용합니다.

cargo llvm-cov --cobertura --output-path cobertura.xml

HTML 리포트로 시각적 확인

커버리지 숫자만 보면 감이 잘 안 올 수 있는데, HTML 리포트를 열어보면 상황이 확 달라집니다. 간단한 예제로 살펴볼게요.

src/lib.rs
pub fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("0으로 나눌 수 없습니다".to_string())
    } else {
        Ok(a / b)
    }
}

pub fn categorize(score: u32) -> &'static str {
    match score {
        0..=59 => "F",
        60..=69 => "D",
        70..=79 => "C",
        80..=89 => "B",
        90..=100 => "A",
        _ => "유효하지 않은 점수",
    }
}

이 코드에 대해 테스트를 다음처럼 작성했다고 해볼게요.

tests/lib_test.rs
use my_project::{categorize, divide};

#[test]
fn test_divide_normal() {
    assert_eq!(divide(10.0, 2.0), Ok(5.0));
}

#[test]
fn test_categorize_a() {
    assert_eq!(categorize(95), "A");
}

#[test]
fn test_categorize_b() {
    assert_eq!(categorize(85), "B");
}

cargo llvm-cov --html --open을 실행하면 브라우저에서 divide 함수의 에러 분기(b == 0.0)가 빨간색으로 표시되고, categorize 함수의 C, D, F 분기와 범위 밖 처리도 빨간색으로 나타납니다. 이걸 보면 “아, 여기 테스트를 더 추가해야겠다”는 판단이 바로 서죠.

특정 테스트만 커버리지 측정

프로젝트가 커지면 전체 테스트를 돌리는 데 시간이 오래 걸릴 수 있습니다. 특정 테스트나 모듈의 커버리지만 보고 싶을 때는 cargo test에서 쓰던 필터링 옵션을 그대로 쓸 수 있어요.

특정 테스트 함수만 실행하려면 -- 뒤에 테스트 이름 패턴을 넣으면 됩니다.

cargo llvm-cov -- test_divide

특정 테스트 파일이나 통합 테스트만 실행할 수도 있고요.

# 특정 통합 테스트 파일만 실행
cargo llvm-cov --test integration_test

# 단위 테스트만 실행 (통합 테스트 제외)
cargo llvm-cov --lib

doc 테스트의 커버리지도 측정할 수 있습니다. 기본적으로 doc 테스트는 커버리지 측정에서 제외되는데, --doctests 플래그를 추가하면 포함돼요.

cargo llvm-cov --doctests

커버리지 임계값 설정

“커버리지가 일정 수준 이하로 떨어지면 빌드를 실패시키고 싶다”는 요구사항은 꽤 흔합니다. cargo-llvm-cov는 --fail-under-lines 옵션으로 이걸 지원해요.

cargo llvm-cov --fail-under-lines 80

이렇게 하면 라인 커버리지가 80% 미만일 때 종료 코드가 0이 아닌 값을 반환해서 CI를 실패시킵니다. 라인 커버리지 외에도 함수 커버리지와 리전 커버리지에 대한 임계값도 설정할 수 있습니다.

# 함수 커버리지 임계값
cargo llvm-cov --fail-under-functions 90

# 리전 커버리지 임계값
cargo llvm-cov --fail-under-regions 75

여러 임계값을 동시에 지정하면 모든 조건을 만족해야 통과합니다.

cargo llvm-cov --fail-under-lines 80 --fail-under-functions 90

주의할 점이 있는데, 처음부터 너무 높은 임계값을 설정하면 팀원들이 의미 없는 테스트를 양산하게 될 수 있습니다. 현재 프로젝트의 커버리지를 먼저 확인하고, 거기서 조금씩 올려가는 게 현실적이에요.

CI에서 활용하기

cargo-llvm-cov는 CI 환경에서 진가를 발휘합니다. PR마다 자동으로 커버리지를 측정하고, 외부 서비스와 연동해서 커버리지 변화를 추적할 수 있거든요.

가장 기본적인 GitHub Actions 워크플로우는 이렇게 생겼습니다.

.github/workflows/coverage.yml
name: Coverage

on:
  push:
    branches: [main]
  pull_request:

jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview
      - uses: taiki-e/install-action@cargo-llvm-cov
      - run: cargo llvm-cov --fail-under-lines 80

taiki-e/install-action을 사용하면 미리 빌드된 바이너리를 받아오기 때문에 CI에서 컴파일하는 시간을 절약할 수 있습니다.

Codecov와 연동하려면 lcov 리포트를 생성해서 업로드하면 됩니다.

.github/workflows/coverage.yml
name: Coverage

on:
  push:
    branches: [main]
  pull_request:

jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview
      - uses: taiki-e/install-action@cargo-llvm-cov
      - name: Generate coverage
        run: cargo llvm-cov --lcov --output-path lcov.info
      - name: Upload to Codecov
        uses: codecov/codecov-action@v4
        with:
          files: lcov.info
        env:
          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

Codecov를 연동하면 PR에 커버리지 변화를 코멘트로 달아주고, 어떤 파일의 커버리지가 올라갔는지 내려갔는지를 시각적으로 보여줍니다. 코드 리뷰할 때 “이 PR이 커버리지를 떨어뜨리는군”하고 바로 확인할 수 있어서 편리해요.

Coveralls를 쓰고 있다면 비슷한 방식으로 연동할 수 있습니다.

- name: Upload to Coveralls
  uses: coverallsapp/github-action@v2
  with:
    file: lcov.info

워크스페이스에서 사용하기

Cargo 워크스페이스를 쓰고 있다면 전체 워크스페이스의 커버리지를 한 번에 측정할 수 있습니다.

cargo llvm-cov --workspace

특정 패키지만 측정하고 싶다면 --package 옵션을 사용합니다.

cargo llvm-cov --package my-core-lib

여러 패키지를 지정할 수도 있어요.

cargo llvm-cov --package my-core-lib --package my-client

반대로 특정 패키지를 제외하고 싶다면 --exclude 옵션을 씁니다. 벤치마크 크레이트나 예제 크레이트처럼 커버리지를 측정할 필요가 없는 패키지를 빼놓을 때 유용해요.

cargo llvm-cov --workspace --exclude my-benchmarks

워크스페이스에서 HTML 리포트를 생성하면 모든 패키지의 커버리지가 하나의 리포트에 통합되어 나옵니다. 패키지 간 경계를 넘나드는 코드 경로도 추적되니까 전체적인 테스트 현황을 파악하기 좋습니다.

커버리지 데이터 정리하기

cargo-llvm-cov는 커버리지 측정을 위해 별도의 빌드 아티팩트를 생성합니다. 이전 실행의 데이터가 남아 있으면 결과가 섞일 수 있는데 --clean 플래그로 이전 데이터를 정리한 뒤 측정할 수 있어요.

cargo llvm-cov clean
cargo llvm-cov

아니면 한 번에 처리할 수도 있습니다.

cargo llvm-cov clean --workspace && cargo llvm-cov --workspace

CI에서는 매번 새로운 환경에서 실행되니까 보통 신경 쓸 필요가 없지만, 로컬에서 반복적으로 측정하다 보면 가끔 이전 데이터 때문에 결과가 이상해질 수 있습니다. 그럴 때 clean을 한 번 돌려주면 해결돼요.

특정 코드 제외하기

커버리지 측정에서 특정 코드를 제외하고 싶은 경우가 있습니다. 디버그용 출력이나 에러 메시지 포매팅처럼 테스트하기 어렵거나 테스트할 가치가 낮은 코드가 대표적이죠.

cfg(not(coverage_nightly)) 속성을 사용하면 특정 코드를 커버리지 측정에서 제외할 수 있습니다. 다만 이 기능은 nightly 컴파일러가 필요해요.

좀 더 실용적인 방법은 #[cfg(not(tarpaulin_include))]와 비슷하게, LLVM의 소스 기반 커버리지에서는 함수나 모듈 단위로 커버리지를 해석하는 거예요. cargo-llvm-cov는 --ignore-filename-regex 옵션으로 특정 파일 패턴을 제외할 수 있습니다.

# 테스트 파일 자체는 커버리지에서 제외
cargo llvm-cov --ignore-filename-regex "tests/"

# 여러 패턴 제외
cargo llvm-cov --ignore-filename-regex "tests/|benches/|examples/"

생성된 코드(빌드 스크립트가 만든 코드 등)나 서드파티 매크로가 생성한 코드를 제외할 때도 유용합니다.

실전 팁

커버리지 도구를 써보면서 느낀 점 몇 가지를 정리해볼게요.

커버리지 숫자 자체에 집착하지 마세요. 100% 커버리지가 버그 없는 코드를 의미하지는 않습니다. 커버리지가 높아도 경계값 테스트가 빠져 있으면 의미가 없고, 커버리지가 낮아도 핵심 로직이 잘 테스트되어 있으면 충분할 수 있어요. 커버리지는 “테스트가 부족한 곳을 찾아주는 도구”이지 “코드 품질의 절대 지표”가 아닙니다.

커버리지 리포트를 활용하는 가장 좋은 방법은 “어디를 테스트하지 않았는지”를 확인하는 겁니다. HTML 리포트에서 빨간색으로 표시된 줄들을 살펴보면서 “여기는 왜 테스트가 없지?”라고 묻는 거예요. 에러 처리 분기, 엣지 케이스, 패닉이 발생할 수 있는 곳이 빨간색이라면 테스트를 추가하는 게 좋습니다. 반면 단순한 getter나 Display 구현 같은 건 굳이 테스트하지 않아도 괜찮을 수 있어요.

CI에 커버리지 임계값을 도입할 때는 팀과 상의해서 현실적인 수치를 정하세요. 기존 프로젝트에 갑자기 90% 임계값을 설정하면 모든 PR이 실패하게 됩니다. 먼저 현재 커버리지를 측정하고, 그 수치에서 약간 아래(예: 현재 75%라면 70%)로 임계값을 설정한 뒤 점진적으로 올려가는 게 현실적이에요.

cargo clippy와 함께 쓰면 코드 품질 파이프라인이 더 견고해집니다. Clippy가 코드의 정적 분석을 담당하고, cargo-llvm-cov가 테스트의 동적 분석을 담당하는 셈이니까요. CI에서 두 도구를 같이 돌리면 린트와 커버리지를 한 번에 챙길 수 있습니다.

.github/workflows/quality.yml
name: Code Quality

on:
  push:
    branches: [main]
  pull_request:

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy, llvm-tools-preview
      - uses: taiki-e/install-action@cargo-llvm-cov
      - run: cargo clippy -- -D warnings
      - run: cargo llvm-cov --fail-under-lines 80

마치며

cargo-llvm-cov는 Rust 프로젝트에서 테스트 커버리지를 측정하는 가장 실용적인 도구입니다. LLVM의 소스 기반 커버리지를 활용하기 때문에 정확한 결과를 제공하고 여러 리포트 형식을 지원해서 로컬 개발부터 CI 연동까지 유연하게 쓸 수 있어요.

처음에는 cargo llvm-cov --html --open으로 HTML 리포트를 열어보면서 어떤 코드가 테스트되지 않았는지 확인하는 것부터 시작해보세요. 빨간색으로 표시된 줄들이 테스트를 추가할 힌트가 되어줄 겁니다. CI에 연동해서 Codecov 같은 서비스와 함께 쓰면 PR마다 커버리지 변화를 자동으로 추적할 수도 있고요.

다만 커버리지 숫자에 매몰되지 않는 게 중요합니다. 의미 있는 테스트를 작성하는 것이 먼저이고, 커버리지는 그 과정에서 사각지대를 찾아주는 보조 도구로 활용하는 게 가장 효과적입니다.

CLI 옵션과 사용 예제는 cargo-llvm-cov GitHub 저장소에서 확인할 수 있습니다.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord