dart-generate-test-mocks について
dart-generate-test-mocksは、モックを使うDartテスト向けのスキルです。フレームワークの選択、モック生成、振る舞いの設定、呼び出しの検証を扱います。
主な機能
- 01mocktailとmockitoの比較
- 02依存性注入を用いたコード構成
- 03build_runnerによるmockitoクラス生成
- 04非同期メソッドの適切なスタブ設定
- 05モック呼び出しの検証
活用シーン
- 01Dartテスト用モックフレームワークの選択
- 02サービス契約のモック生成
- 03非同期サービスの動作テスト
- 04呼び出し回数と想定外の操作の確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ dhruvanbhalara/ skills --skill dart-generate-test-mocksインストール後、Claude Code で/dart-generate-test-mocksを入力して読み込まれたか確認します。
詳細
目次
モックフレームワークの選択
| 基準 | mocktail(推奨) | mockito |
|---|---|---|
| コード生成 | 不要 | build_runner が必要 |
| セットアップの手間 | 最小限(class MockService extends Mock implements Service {}) | アノテーション + ジェネレーターのビルドステップ |
| 型安全性 | 実行時の検証 | コンパイル時に生成されるモッククラス |
| 複雑な API | 手動でのフォールバック値(registerFallbackValue) | @GenerateNiceMocks による自動処理 |
テスト可能なコードの構造化
効果的な単体テストを書くには、依存性注入を使ってコードベースを構造化します。物理的なレイヤー(ディスクストレージ、外部サーバー、プラットフォームチャネルなど)とやり取りする操作を分離し、テスト時にモックオブジェクトへ差し替えられるようにします。
- コンストラクターインジェクション: すべてのサービスの依存関係(例: HTTP クライアント、データベースヘルパークライアント)は、クラス内で直接インスタンス化するのではなく、クラスのコンストラクターに渡します。
- インターフェース分離: サービスの契約を表す明確な抽象基底クラスを定義します。モックは、具体的なサービス実装ではなく、理想的にはこれらの抽象的な契約を対象にすべきです。
開発用依存関係の管理
pubspec.yaml を設定して、コード生成とモックテストに必要なパッケージを指定します。
dev_dependenciesの下に対象のテストフレームワークとジェネレーターを追加します:dart pub add dev:test dev:mockito dev:build_runner- テストファイル内で Mockito のアノテーションをインポートします:
import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart';
モックファイルの生成
package:mockito と build_runner を併用して、モックの構造を自動生成します。
- GenerateNiceMocks: 従来の
@GenerateMocksではなく、常に@GenerateNiceMocksアノテーションを使用します。nice モックは、事前にスタブを設定せずにメソッドが呼び出された場合に「MissingStubException」をスローする代わりに、null または一致するデフォルト値を自動的に返します。 - MockSpec の設定: テストファイルのエントリポイントに
@GenerateNiceMocks([MockSpec<YourService>()])を付与します。 - ミラー拡張のインポート:
.mocks.dartサフィックスを使って、対応する生成ファイルをインポートします(例:import 'service_test.mocks.dart')。 - ジェネレーターの実行: コマンドラインからコード生成を実行します:
dart run build_runner build --delete-conflicting-outputs
スタブと検証のベストプラクティス
次のガイドラインに従って、信頼性の高いモックのやり取りを記述します。
- Future と Stream のスタブ:
FutureまたはStreamを返すメソッドをスタブする場合は、必ず.thenAnswer((_) async => value)を使用します。非同期の戻り値に.thenReturn()を使うと実行時のキャストエラーが発生するため、決して使用しないでください。 - 呼び出しの追跡:
verify()API を使って、特定のメソッドが呼び出されたことを検証します。正確な呼び出し回数をアサートするには.called(number)を呼び出します。 - 未使用のアサーション:
verifyNever()またはverifyNoMoreInteractions(mock)を使って、テストのライフサイクル中に予期しない操作が発生しなかったことを保証します。
ワークフロー: モックの生成とテストの検証
モック設定を確立するには、次のチェックリストに従ってください:
- コンストラクタの確認: 対象のサービスクラスがコンストラクタ経由で依存関係を受け入れることを確認します。
- テストファイルの作成:
test/feature_test.dartを作成し、Mockito とテスト用ライブラリをインポートします。 - エントリポイントへのアノテーション付与:
main()の上に@GenerateNiceMocks([MockSpec<TargetService>()])を追加します。 - モックのインポート宣言:
feature_test.mocks.dartを対象とするインポート文を追加します。 - コードの生成: ターミナルで
dart run build_runner buildを実行し、モック化されたファイルを生成します。 - モックのインスタンス化:
setUpコールバック内で、生成されたモッククラスをインスタンス化します(例:mockService = MockTargetService())。 - スタブの適用:
when()を使用して、テストケース内で振る舞いを設定します。 - 実行とアサート: テスト対象のシステムを呼び出し、
expect()で出力を検証します。 - 呼び出しの検証:
verify()を使用して、モックとのやり取りをアサートします。
例
完全なモック化テストスイート(Mockito)
この例では、リモート API クライアントをモックして、データベース同期サービスをテストする方法を示します。
import 'package:test/test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:http/http.dart' as http;
// Define an abstract contract for a user repository
abstract class UserRepository {
Future<String> getUserName(int id);
}
// System Under Test consuming the repository
class UserService {
final UserRepository repository;
UserService(this.repository);
Future<String> fetchDisplayName(int id) async {
try {
final name = await repository.getUserName(id);
return 'User: $name';
} catch (_) {
return 'Unknown User';
}
}
}
// 1. Annotate to generate MockUserRepository
@GenerateNiceMocks([MockSpec<UserRepository>()])
import 'user_service_test.mocks.dart';
void main() {
group('UserService', () {
late MockUserRepository mockRepo;
late UserService userService;
setUp(() {
mockRepo = MockUserRepository();
userService = UserService(mockRepo);
});
test('returns formatted display name when repository succeeds', () async {
// 2. Arrange: Stub the async getUserName method
when(mockRepo.getUserName(42)).thenAnswer(
(_) async => 'Alice',
);
// 3. Act: Run target method
final displayName = await userService.fetchDisplayName(42);
// 4. Assert: Validate outcomes
expect(displayName, equals('User: Alice'));
// 5. Verify: Assert the repository method was invoked with correct arguments
verify(mockRepo.getUserName(42)).called(1);
});
test('returns fallback string when repository throws exception', () async {
// Arrange: Stub to throw error
when(mockRepo.getUserName(any)).thenThrow(
Exception('Connection Timeout'),
);
// Act
final displayName = await userService.fetchDisplayName(99);
// Assert
expect(displayName, equals('Unknown User'));
});
});
}
コメント
Markdown 対応