关于 flutter-testing-skill
flutter-testing-skill 面向 Flutter 开发者,介绍单元、Widget、集成和 Golden 测试,并提供 Dart 示例、测试命令及查找器、交互和依赖模拟指南。
主要功能
- 01单元、Widget 与集成测试指南
- 02Widget 查找器和交互模式
- 03用于视觉回归的 Golden 测试
- 04使用 Mockito 模拟依赖
- 05运行测试和管理测试的命令
适用场景
- 01测试 Flutter 界面行为与交互
- 02将界面与 Golden 图像进行比对
- 03运行集成测试或生成模拟对象
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ lambdatest/ agent-skills --skill flutter-testing-skill安装后在 Claude Code 中输入/flutter-testing-skill确认已加载。
附带文件reference/advanced-patterns.mdreference/playbook.md
详细介绍
Flutter 测试技能
你是一名专注于测试的资深 Flutter 开发者。
步骤 1 — 测试类型
├─ "unit test", "business logic", "model test"
│ └─ Unit test: test/ directory, flutter_test package
│
├─ "widget test", "component test", "UI test"
│ └─ Widget test: test/ directory, testWidgets()
│
├─ "integration test", "E2E", "full app test"
│ └─ Integration test: integration_test/ directory
│
├─ "golden test", "snapshot", "visual regression"
│ └─ Golden test: matchesGoldenFile()
│
└─ Ambiguous? → Widget test (most common)
核心模式 — Dart
Widget 测试(最常见)
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/screens/login_screen.dart';
void main() {
testWidgets('Login screen shows email and password fields', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(home: LoginScreen()));
// Verify fields exist
expect(find.byType(TextField), findsNWidgets(2));
expect(find.text('Email'), findsOneWidget);
expect(find.text('Password'), findsOneWidget);
expect(find.byType(ElevatedButton), findsOneWidget);
});
testWidgets('Login with valid credentials navigates to dashboard', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(home: LoginScreen()));
// Enter credentials
await tester.enterText(find.byKey(const Key('emailField')), '[email protected]');
await tester.enterText(find.byKey(const Key('passwordField')), 'password123');
// Tap login button
await tester.tap(find.byKey(const Key('loginButton')));
await tester.pumpAndSettle(); // Wait for animations and navigation
// Verify navigation
expect(find.text('Dashboard'), findsOneWidget);
});
testWidgets('Shows error for invalid credentials', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(home: LoginScreen()));
await tester.enterText(find.byKey(const Key('emailField')), '[email protected]');
await tester.enterText(find.byKey(const Key('passwordField')), 'wrong');
await tester.tap(find.byKey(const Key('loginButton')));
await tester.pumpAndSettle();
expect(find.text('Invalid credentials'), findsOneWidget);
});
}
查找器策略
// By Key (best — explicit test identifiers)
find.byKey(const Key('loginButton'))
find.byKey(const ValueKey('email_input'))
// By Type
find.byType(ElevatedButton)
find.byType(TextField)
find.byType(LoginScreen)
// By Text
find.text('Login')
find.textContaining('Welcome')
// By Icon
find.byIcon(Icons.login)
// By Widget predicate
find.byWidgetPredicate((widget) => widget is Text && widget.data!.startsWith('Error'))
// Descendant/Ancestor
find.descendant(of: find.byType(AppBar), matching: find.text('Title'))
find.ancestor(of: find.text('Login'), matching: find.byType(Card))
操作
await tester.tap(finder); // Tap
await tester.longPress(finder); // Long press
await tester.enterText(finder, 'text'); // Type text
await tester.drag(finder, const Offset(0, -300)); // Drag/scroll
await tester.fling(finder, const Offset(0, -500), 1000); // Fling/swipe
// CRITICAL: Always pump after actions
await tester.pump(); // Single frame
await tester.pump(const Duration(seconds: 1)); // Advance time
await tester.pumpAndSettle(); // Wait for animations to finish
集成测试
// integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Full login flow', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
// Login
await tester.enterText(find.byKey(const Key('emailField')), '[email protected]');
await tester.enterText(find.byKey(const Key('passwordField')), 'password123');
await tester.tap(find.byKey(const Key('loginButton')));
await tester.pumpAndSettle();
// Verify dashboard
expect(find.text('Dashboard'), findsOneWidget);
// Navigate to settings
await tester.tap(find.byIcon(Icons.settings));
await tester.pumpAndSettle();
expect(find.text('Settings'), findsOneWidget);
});
}
Golden 测试(视觉回归)
testWidgets('Login screen matches golden', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(home: LoginScreen()));
await tester.pumpAndSettle();
await expectLater(
find.byType(LoginScreen),
matchesGoldenFile('goldens/login_screen.png'),
);
});
# Generate golden files
flutter test --update-goldens
# Run golden comparison
flutter test
模拟依赖项
// Using Mockito
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([AuthService])
void main() {
late MockAuthService mockAuth;
setUp(() {
mockAuth = MockAuthService();
});
testWidgets('Login calls auth service', (tester) async {
when(mockAuth.login(any, any)).thenAnswer((_) async => true);
await tester.pumpWidget(MaterialApp(
home: LoginScreen(authService: mockAuth),
));
await tester.enterText(find.byKey(const Key('emailField')), '[email protected]');
await tester.enterText(find.byKey(const Key('passwordField')), 'pass123');
await tester.tap(find.byKey(const Key('loginButton')));
await tester.pumpAndSettle();
verify(mockAuth.login('[email protected]', 'pass123')).called(1);
});
}
反模式
| 不佳做法 | 良好做法 | 原因 |
|---|---|---|
操作后不调用 pumpAndSettle() | 交互后始终调用 pump | 动画尚未完成 |
对动态文本使用 find.text() | 使用 find.byKey() | 本地化或文本变更会导致测试失败 |
| 测试实现细节 | 测试面向用户的行为 | 脆弱 |
| Widget 测试中不使用模拟 | 模拟服务和仓库 | 测试会调用真实 API |
TestMu AI Cloud(集成测试)
# Run integration tests on LambdaTest real devices
# 1. Build app for testing
flutter build apk --debug # Android
flutter build ios --simulator # iOS
# 2. Upload to LambdaTest
curl -u "$LT_USERNAME:$LT_ACCESS_KEY" \
-X POST "https://manual-api.lambdatest.com/app/upload/realDevice" \
-F "appFile=@build/app/outputs/flutter-apk/app-debug.apk"
# 3. Run via Appium (Flutter driver)
# Use appium-flutter-driver for element interaction
快速参考
| 任务 | 命令 |
|---|---|
| 运行所有测试 | flutter test |
| 运行指定文件 | flutter test test/login_test.dart |
| 运行并生成覆盖率 | flutter test --coverage |
| 运行集成测试 | flutter test integration_test/ |
| 更新 goldens | flutter test --update-goldens |
| 生成模拟对象 | flutter pub run build_runner build |
| 测试指定平台 | flutter test --platform chrome |
pubspec.yaml
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
mockito: ^5.4.0
build_runner: ^2.4.0
深入模式
有关高级模式、调试指南、CI/CD 集成和最佳实践,
请参阅 reference/playbook.md。
评论
支持 Markdown