iBetter Books
수정

코드 커버리지 측정과 개선

"테스트를 얼마나 작성했는가"는 테스트 수로 측정하지 않습니다. 코드 커버리지(Coverage)로 측정합니다. 커버리지는 테스트가 실행한 코드의 비율입니다. 100줄 중 80줄을 테스트가 실행했다면 80% 커버리지입니다.

커버리지가 높다고 버그가 없다는 뜻은 아닙니다. 하지만 커버리지가 낮으면 테스트되지 않은 코드가 많다는 신호입니다.

dart test --coverage

dart test--coverage 옵션을 주면 커버리지 데이터를 수집합니다.

dart test --coverage=coverage

coverage/ 디렉토리에 .json 형식의 커버리지 데이터가 생성됩니다.

lcov 리포트 생성

dart pub global activate coverageformat_coverage 도구를 설치합니다.

# coverage 도구 전역 설치dart pub global activate coverage# lcov 형식으로 변환dart pub global run coverage:format_coverage \  --lcov \  --in=coverage \  --out=coverage/lcov.info \  --packages=.dart_tool/package_config.json \  --report-on=lib

lcov.info 파일이 생성됩니다. 이 파일을 시각화 도구로 읽을 수 있습니다.

HTML 리포트 보기

genhtml로 HTML 리포트를 생성합니다.

# macOSbrew install lcov# Ubuntu/Debiansudo apt-get install lcov# HTML 리포트 생성genhtml coverage/lcov.info -o coverage/html# 브라우저로 열기open coverage/html/index.html

HTML 리포트에서는 파일별 커버리지와 실행되지 않은 코드 라인을 빨간색으로 확인할 수 있습니다.

Makefile에 커버리지 타깃 추가

# 수정: Makefile
APP_NAME := file_organizer
BUILD_DIR := build
ENTRY := bin/file_organizer.dart

.PHONY: build clean test coverage install

## 네이티브 실행 파일 빌드
build:
	@mkdir -p $(BUILD_DIR)
	dart compile exe $(ENTRY) -o $(BUILD_DIR)/$(APP_NAME)
	@echo "빌드 완료: $(BUILD_DIR)/$(APP_NAME)"

## 테스트 실행
test:
	dart test

## 커버리지 측정 및 리포트 생성
coverage:
	dart test --coverage=coverage
	dart pub global run coverage:format_coverage \
		--lcov \
		--in=coverage \
		--out=coverage/lcov.info \
		--packages=.dart_tool/package_config.json \
		--report-on=lib
	genhtml coverage/lcov.info -o coverage/html
	@echo "커버리지 리포트: coverage/html/index.html"

## 개발 모드 실행
run:
	dart run $(ENTRY) $(ARGS)

## /usr/local/bin에 설치
install: build
	cp $(BUILD_DIR)/$(APP_NAME) /usr/local/bin/$(APP_NAME)
	@echo "설치 완료: /usr/local/bin/$(APP_NAME)"

## 빌드 아티팩트 삭제
clean:
	rm -rf $(BUILD_DIR) coverage
make coverage

커버리지 분석 — 낮은 부분 찾기

리포트를 확인하면 커버리지가 낮은 파일을 찾을 수 있습니다. 일반적으로 다음 코드가 낮게 나옵니다.

에러 처리 분기

// 이 경우를 테스트하지 않았다면 커버리지 낮음
try {
  await source.rename(destPath);
} on FileSystemException catch (e) {
  // ← 이 줄이 빨간색
  throw OrganizeException('파일 이동 실패: ${e.message}');
}

테스트에서 의도적으로 예외를 발생시키면 커버리지가 올라갑니다.

test('파일 이동 실패 처리', () async {
  // 존재하지 않는 파일 이동 시도
  final nonExistent = File('/nonexistent/file.txt');

  await expectLater(
    () => moveFileSafely(nonExistent, '/tmp'),
    throwsA(isA<FileSystemException>()),
  );
});

dead code 발견

String extensionToCategory(String ext) {
  const categories = {
    '.heic': '이미지',  // ← 테스트에서 한 번도 호출 안 됨
    // ...
  };
  return categories[ext] ?? '기타';
}

커버리지가 낮은 분기는 두 가지 중 하나입니다. 테스트를 추가해야 하는 경우이거나, 실제로 사용되지 않는 코드(dead code)인 경우입니다.

커버리지 목표 설정

커버리지 목표는 프로젝트 성격에 따라 다릅니다.

코드 종류 권장 목표
핵심 비즈니스 로직 90% 이상
유틸리티 함수 80% 이상
UI/커맨드 레이어 60% 이상
전체 평균 70% 이상

커버리지 100%는 현실적이지 않고 권장하지 않습니다. 에러 처리나 엣지 케이스를 억지로 테스트하면 오히려 유지보수가 어려워집니다.

GitHub Actions에 커버리지 통합

# 수정: .github/workflows/build.ymlname: CIon:  push:    branches: [main]  pull_request:    branches: [main]jobs:  test:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - uses: dart-lang/setup-dart@v1        with:          sdk: stable      - name: 의존성 설치        run: dart pub get      - name: 테스트 및 커버리지 수집        run: dart test --coverage=coverage      - name: 커버리지 변환        run: |          dart pub global activate coverage          dart pub global run coverage:format_coverage \            --lcov \            --in=coverage \            --out=coverage/lcov.info \            --packages=.dart_tool/package_config.json \            --report-on=lib      - name: Codecov 업로드        uses: codecov/codecov-action@v4        with:          file: coverage/lcov.info  build:    needs: test    strategy:      matrix:        include:          - os: ubuntu-latest            output: file_organizer-linux          - os: macos-latest            output: file_organizer-macos          - os: windows-latest            output: file_organizer-windows.exe    runs-on: ${{ matrix.os }}    steps:      - uses: actions/checkout@v4      - uses: dart-lang/setup-dart@v1        with:          sdk: stable      - run: dart pub get      - run: mkdir -p build && dart compile exe bin/file_organizer.dart -o build/${{ matrix.output }}      - uses: actions/upload-artifact@v4        with:          name: ${{ matrix.output }}          path: build/${{ matrix.output }}

커버리지 뱃지

README에 커버리지 뱃지를 추가합니다. Codecov에 연동하면 자동으로 뱃지 URL을 제공합니다.

[![Coverage](https://codecov.io/gh/your-org/file_organizer/branch/main/graph/badge.svg)](https://codecov.io/gh/your-org/file_organizer)

커버리지 개선 전략

낮은 커버리지를 개선할 때 효과적인 접근 방법입니다.

1. 테이블 드리븐 테스트

비슷한 입력/출력 쌍이 많을 때 효과적입니다.

test('extensionToCategory 전체 케이스', () {
  final cases = {
    '.jpg': '이미지',
    '.jpeg': '이미지',
    '.png': '이미지',
    '.gif': '이미지',
    '.mp4': '동영상',
    '.mov': '동영상',
    '.pdf': '문서',
    '.txt': '문서',
    '.dart': '코드',
    '.py': '코드',
    '.zip': '압축',
    '.unknown': '기타',
    '': '기타',
  };

  for (final entry in cases.entries) {
    expect(
      extensionToCategory(entry.key),
      equals(entry.value),
      reason: '확장자 ${entry.key}',
    );
  }
});

2. 에러 경로 명시적 테스트

group('에러 처리', () {
  test('빈 규칙 목록 CSV 파싱', () {
    final rules = parseRulesFromCsv('pattern,replacement\n');
    expect(rules, isEmpty);
  });

  test('헤더만 있는 CSV', () {
    final rows = parseCsv('name,age');
    expect(rows, isEmpty);
  });
});

PART 05 마무리

PART 05에서 테스트의 전체 스펙트럼을 다뤘습니다.

  • Ch 01: test 패키지 기초 — test(), expect(), group(), setUp()
  • Ch 02: 단위 테스트 — 순수 함수, 클래스 메서드, 비동기
  • Ch 03: Mock — mockito로 의존성 격리, 의존성 주입 패턴
  • Ch 04: 통합/E2E 테스트 — 실제 파일 시스템, 프로세스 실행
  • Ch 05: 커버리지 — 측정, 리포트, CI 통합

PART 06에서는 새로운 프로젝트, 할 일 관리 REST API 서버를 dart_frog로 만듭니다.