Flutter のテストカバレッジを 80% に上げる — 計測の落とし穴と実践パターン
「ぽんっ!」(旧称:でいりんぐ)のテストカバレッジを 60% 台から 80% に引き上げました。数字を追う前に計測そのものの落とし穴でつまずいたので、そこと実践パターンを残します。
背景(当時の状況)
カバレッジ基準を 80% に定めていたものの、実測は 60% 台にとどまっていました(2026-07-11 時点)。ところが数字を上げにかかる前に、そもそも「測り方」で数字が過小に出ていることに気づきます。まず計測の落とし穴を潰してからで 80% を超えるところまで持っていきました。
落とし穴①: 生成物を分母に入れると過小表示される
Flutter/Dart は生成コードが多いプロジェクトです。これらをカバレッジの分母に含めると、実力より大幅に低く出ます。
app_localizations*.dart(多言語)、*.g.dart、*.freezed.dart、*.gr.dartなどの生成物は評価から除外する(lcov.infoのSF:(ソースファイル)行を見て、生成物のパスを手元で除外して集計する)。- 「ぽんっ!」では、生成物込みだと 40% 台に見えていたものが、除外すると実カバレッジになりました。
「数字が低い」の前に、何を分母にしているかを疑うべきでした。
落とし穴②: fresh worktree での計測は不正確
新しく git worktree を切った直後などに flutter test --coverage を回すと、lcov が不完全になることがあります。widget/screenshot テストが多くの lib ファイルを読み込まず、総行数が実際の 1/5 程度しか出ず、見かけ上のカバレッジが高く(あるいは低く)誤表示されました。
- 依存や生成物が揃った主リポジトリで計測する。
- worktree では、まず
build_runnerで生成物を作ってから測る。
実践パターン
数字を上げるうえで効いた具体策です。
リポジトリ層は fake を注入する
// FakeFirebaseFirestore をコンストラクタ注入してユニットテスト
final repo = FirestoreDailyCardRepository(FakeFirebaseFirestore());
FirebaseAuth は手頃な mock パッケージが無いので、mocktail で Mock implements FirebaseAuth を作って注入します。
autoDispose プロバイダは listen で保持してから await
// .future を直接 read すると「disposed during loading」で hang する
container.listen(myProvider, (_, __) {}); // 先に保持
final value = await container.read(myProvider.future);
無限アニメーション画面では pumpAndSettle を使わない
CircularProgressIndicator のように終わらないアニメーションがある画面で pumpAndSettle() を呼ぶとタイムアウトします。時間を指定した pump に置き換えます。
await tester.pump(const Duration(milliseconds: 300));
日付整形テストは初期化を await する
setUpAll(() async {
await initializeDateFormatting('ja'); // await しないと raw フォールバックする
});
テストしにくい所は integration test に寄せる
すべてを unit/widget で賄う必要はありません。カメラプレビュー依存の画面や、ネイティブライブラリ依存のリポジトリ(ObjectBox など)は注入点が無く、無理に unit で書くより integration test で担保するほうが素直でした。カバレッジ目標は「全ファイル一律」ではなく、テスト種別ごとの適材適所で達成します。
まとめ
- カバレッジは測り方で数字が変わる。生成物を分母から除外し、依存の揃った主リポで計測する。
- 数字を上げる本体は fake/mock 注入。autoDispose は
listenで保持、無限アニメ画面は時間指定pump、日付整形は初期化await。 - テストしにくい箇所は integration test に寄せる。unit/widget/integration の役割分担でカバレッジ目標に届かせるのが現実的でした。