코드 커버리지 측정과 개선
"테스트를 얼마나 작성했는가"는 테스트 수로 측정하지 않습니다. 코드 커버리지(Coverage)로 측정합니다. 커버리지는 테스트가 실행한 코드의 비율입니다. 100줄 중 80줄을 테스트가 실행했다면 80% 커버리지입니다.
커버리지가 높다고 버그가 없다는 뜻은 아닙니다. 하지만 커버리지가 낮으면 테스트되지 않은 코드가 많다는 신호입니다.
dart test --coverage
dart test에 --coverage 옵션을 주면 커버리지 데이터를 수집합니다.
dart test --coverage=coverage
coverage/ 디렉토리에 .json 형식의 커버리지 데이터가 생성됩니다.
lcov 리포트 생성
dart pub global activate coverage로 format_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을 제공합니다.
[](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로 만듭니다.