testing について
Dart・Flutter開発者向けに、VGVの規約を単体テストとウィジェットテストへ適用するスキルです。読みやすい構成、テスト分離、mocktail、ウィジェット用ヘルパー、ゴールデンテスト、実行方法を扱います。
主な機能
- 01説明的で階層的なテスト命名
- 02分離されたセットアップとプライベートモック
- 03mocktailを使うモック規約
- 04pumpAppによるウィジェットテスト
- 05ゴールデンテストのタグ付けと外観確認
- 06test MCPツールの実行指針
活用シーン
- 01Dartの単体テストを整理
- 02動作を重視したウィジェットテストの作成
- 03テスト間の共有状態を削減
- 04Flutterモノレポでテストを実行
MCP 対応
test MCPツールを使ったテスト実行の指針があり、directoryとtimeoutの指定方法も扱います。Claude Codeでの選択肢としてmcp__very-good-cli__testを挙げ、サーバーがない場合のコマンドラインによる代替も説明します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ verygoodopensource/ vgv-ai-flutter-plugin --skill testingインストール後、Claude Code で/testingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlreferences/animation-testing.mdconfiguration.mdcoverage.mdgolden-tests.mdmatchers.mdmocktail.mdtest-patterns.mdwidget-tests.md詳細
Dart & Flutter のテスト
Dart と Flutter における VGV のテスト規約です。フレームワークの API は前提とし、以下ではその上に重ねるプロジェクト独自のスタイルを示します。
コア標準
すべてのテスト作業で、次の標準を適用してください。
- わかりやすいテスト名 — 動作を説明する、詳細で読みやすい名前を付けます。
'works'や'renders'は使わないでください - 自然な文として読める階層的な group/test 構造 — クラスには最上位の
group、メソッドにはネストしたgroup、動作にはtestを使います(例:UserRepository→getUser→ API が成功したときに User を返す) - 型の参照には文字列補間を使用 —
'returns User'ではなく'returns $User'を使い、名前の変更が自動的に反映されるようにします - モックはファイルごとにプライベートにする — ファイル間の結合を防ぐため、アンダースコアを接頭辞に付けて
class _MockX extends Mock implements X {}と宣言します - テストのセットアップはグループ内にまとめる — すべての
setUp/tearDown呼び出しはgroup内に置き、main()のトップレベルには置かないでください - 可変オブジェクトは
lateを使ってsetUp()で初期化 —late MyDep dep;と宣言してからsetUpで代入し、各テストで新しいインスタンスを使えるようにします - テスト間で可変状態を共有しない — テスト間で状態が保持される静的メンバー、グローバル変数、トップレベルの final インスタンスは使わないでください
package:mocktailを使用 —package:mockitoは使わないでください- テストタグは定数にする —
static constフィールドを持つabstract class TestTagを使い、タグに生の文字列リテラルを渡さないでください - プロパティではなく動作をテスト — ウィジェットテストでは機能上の結果に焦点を当て、静的な視覚プロパティはゴールデンテストで検証します
pumpAppテストヘルパーを使用 —test/helpers/pump_app.dartの共通ヘルパーでウィジェットをラップし、pumpWidget(MaterialApp(...))を直接記述しないでください- すべてのゴールデンテストにタグを付ける — ゴールデンテストを個別に実行・更新できるよう、
TestTag.goldenを付けます - プロジェクトがワークスペースのルートにない場合は、
testMCP ツールにdirectoryを渡す — Flutter プロジェクトがサブディレクトリ(例:mobile/)にあるモノレポでは、directory: 'mobile'が必要です。pubspec.yamlがワークスペースのルートにある場合に限り、省略してください testMCP ツールにtimeout_secondsを渡す — タイムアウトなしでpumpAndSettle()が呼び出されると、Flutter テストが無期限に停止することがあります。停止し続けるのではなく実行が強制終了されるよう、上限(例:timeout_seconds: 120)を設定してくださいtestMCP ツールでクロスハーネスのフォールバックを使用 — Claude Code ではmcp__very-good-cli__testを使用します。このプラグインの Bash フックも MCP サーバーへの接続もない環境では、同じカバレッジおよびタイムアウトのオプションを指定してvery_good test(またはflutter test/dart test)を直接実行してください。MCP サーバーが見つからない場合でも処理を止めないでください
ファイル構成
| 規約 | ルール |
|---|---|
| ファイルの接尾辞 | すべてのテストファイルは _test.dart で終わります |
| ディレクトリ | すべてのテストは test/ の下に置きます |
| 構成のミラー | test/ は lib/ の構成を正確に反映します — lib/src/models/user.dart → test/src/models/user_test.dart |
| ヘルパー | 共通のテストユーティリティは test/helpers/ に置きます(例:pump_app.dart、fakes.dart) |
Group と Test の階層
連結した説明が自然な文として読めるように、グループを構成してください。最上位のグループでは、文字列ではなく PascalCase の型そのものを使います。
class _MockApiClient extends Mock implements ApiClient {}
void main() {
group(UserRepository, () {
late ApiClient apiClient;
late UserRepository subject;
setUp(() {
apiClient = _MockApiClient();
subject = UserRepository(apiClient: apiClient);
});
group('getUser', () {
test('returns $User when API call succeeds', () async {
when(() => apiClient.fetchUser(any()))
.thenAnswer((_) async => User(id: '1', name: 'Dash'));
final result = await subject.getUser('1');
expect(result, equals(User(id: '1', name: 'Dash')));
verify(() => apiClient.fetchUser('1')).called(1);
});
test('throws $UserNotFoundException when API returns 404', () {
when(() => apiClient.fetchUser(any()))
.thenThrow(ApiException(statusCode: 404));
expect(
() => subject.getUser('1'),
throwsA(isA<UserNotFoundException>()),
);
});
});
});
}
命名規則
| パターン | 例 |
|---|---|
| 値を返す | 'returns $User when API call succeeds' |
| 例外をスローする | 'throws $UserNotFoundException when user is not found' |
| 依存関係を呼び出す | 'calls apiClient.deleteUser with correct id' |
| 状態を出力する | 'emits [loading, success] when data is fetched' |
| 条件付きの動作 | 'returns cached value when cache is not expired' |
| エッジケース | 'returns empty list when repository has no items' |
テストの分離
各テストは、単独で、任意の順序で、並列で実行した場合にも合格する必要があります。隠れた依存関係を見つけるには、
--test-randomize-ordering-seed random を使用します。
| アンチパターン | 問題 | 正しい方法 |
|---|---|---|
main() のトップレベルにある setUp | 最適化のためにテストランナーがファイルを統合すると動作しなくなります | setUp を group の中に移動する |
final dep = _MockDep();(トップレベル) | すべてのテストで同じインスタンスが共有され、状態が漏れます | group 内で late と setUp を使用する |
class MockDep extends Mock(パブリック) | 他のテストファイルからインポートされ、依存される可能性があります | class _MockDep extends Mock(プライベート)を使用する |
| 静的/グローバルな可変変数 | テスト間で状態が保持されます | setUp でリセットするか、完全に避ける |
| 特定の順序で実行する必要があるテスト | 不安定で、ランダムな順序では失敗します | 各テストを完全に自己完結させる |
プライベートロジックのテスト
プライベートメソッドを直接テストしてはいけません。プライベートロジックを使用するパブリックメソッドを通じて実行します。
// If _normalizeEmail is private, test it through the public createUser method:
test('normalizes email to lowercase before saving', () async {
when(() => repository.save(any())).thenAnswer((_) async {});
await subject.createUser(email: '[email protected]');
final captured = verify(() => repository.save(captureAny())).captured;
expect(captured.first.email, equals('[email protected]'));
});
ウィジェットのテスト
| ルール | 詳細 |
|---|---|
testWidgets を使用する | すべてのウィジェットテストで、test ではなく testWidgets を使用します |
find.byType を優先する | デフォルトのファインダーです。ユーザーに表示されるコンテンツには find.text を使用し、型やテキストで判別できない場合にのみ find.byKey を使用します |
| 動作のカテゴリごとにグループ化する | ネストしたグループ名には renders、navigates、calls [MethodName]、updates を使用します |
| 動作に注目する | ウィジェットの 動作(テキストの表示、コールバックの呼び出し、画面遷移)を検証します。見た目にはゴールデンテストを使用します |
| Bloc と Cubit をモックする | package:bloc_test の MockBloc/MockCubit を使用します。ウィジェットテストで実際の Bloc を使用してはいけません |
pumpAndSettle() より pump() を優先する | pumpAndSettle は CircularProgressIndicator などの無限アニメーションで停止することがあります |
pumpApp ヘルパー
すべてのウィジェットテストで、共通のヘルパーを通じてテスト対象のウィジェットをラップし、各テストで独自の MaterialApp を直接記述しないようにします。
// test/helpers/pump_app.dart
extension PumpApp on WidgetTester {
Future<void> pumpApp(Widget widget) {
return pumpWidget(
MaterialApp(
home: widget,
),
);
}
}
test/helpers/helpers.dart からエクスポートすると、各テストで1行のインポートを使えます。
import '../helpers/helpers.dart';
void main() {
group(MyWidget, () {
testWidgets('renders greeting text', (tester) async {
await tester.pumpApp(const MyWidget());
expect(find.text('Hello'), findsOneWidget);
});
});
}
ウィジェットテストのアンチパターン
| アンチパターン | 問題 | 正しい方法 |
|---|---|---|
各テストにインラインで MaterialApp を記述 | ボイラープレートが重複し、セットアップに一貫性がなくなります | pumpApp ヘルパーを使用します |
デフォルトのファインダーとして find.byKey を使用 | テストが実装上のキーに依存します | find.byType または find.text を優先します |
| パディング、色、フォントサイズをテスト | 脆弱で、デザインを少し調整するたびに失敗し、動作ではなく見た目を検証します | 見た目の検証にはゴールデンテストを使用します |
インタラクション後に pump() を呼び出さない | ウィジェットツリーが再構築されず、アサーションで古い状態が確認されます | tap、enterText などの後は必ず pump() を呼び出します |
| ウィジェットテストで実際の Bloc を使用 | テストが統合テストになり、遅く、脆く、分離が難しくなります | bloc_test の MockBloc/MockCubit を使用します |
パディング、背景色、フォントサイズは、繰り返しテストしたくなる項目です。ウィジェットテストでこれらをアサートすると、テストがデザインシステムに依存し、スタイルを変更するたびに失敗します。これらは TestTag.golden タグを付けたゴールデンテストに移してください。
その他のリソース
- references/mocktail.md — モックとフェイク、スタブ設定、引数マッチャー、検証、フォールバック値
- references/test-patterns.md —
setUp/tearDownのライフサイクルと、非同期処理、ストリーム、例外、Equatable、コールバックのパターン - references/widget-tests.md — Bloc を使用した完全なウィジェットテスト、
pumpAppでのテーマ/ローカライズ、ポンピングメソッド、ファインダー、インタラクション - references/golden-tests.md — ゴールデンファイルテスト(セットアップ、ゴールデンの作成、タグ付け、実行/更新、アンチパターン)
- references/matchers.md — マッチャーのクイックリファレンス
- references/configuration.md —
dart_test.yamlの設定(タグ、プラットフォームのオーバーライド)と MCPtestツールを使ったテストの実行 - references/coverage.md — カバレッジのパターンとパッケージ/インポートのリファレンス
- references/animation-testing.md — 暗黙的/明示的なアニメーション、AnimatedSwitcher、ページ遷移、注入されたコントローラーのテスト