公告

👇欢迎大家关注公众号&私信交流👇

Skip to content

Flutter Widget 测试详解

简介

Widget 测试用于测试 UI 组件的行为和渲染。本文介绍如何在 Flutter 中编写 Widget 测试。

基本配置

添加依赖

yaml
dev_dependencies:
  flutter_test:
    sdk: flutter

创建测试文件

dart
// test/widget_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/my_widget.dart';

void main() {
  testWidgets('MyWidget should display a title', (tester) async {
    await tester.pumpWidget(MyWidget(title: 'Hello'));
    expect(find.text('Hello'), findsOneWidget);
  });
}

测试工具

WidgetTester

dart
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
  // 构建 Widget
  await tester.pumpWidget(MyApp());
  
  // 查找组件
  expect(find.text('0'), findsOneWidget);
  expect(find.text('1'), findsNothing);
  
  // 触发点击
  await tester.tap(find.byIcon(Icons.add));
  
  // 等待动画完成
  await tester.pumpAndSettle();
  
  // 验证结果
  expect(find.text('0'), findsNothing);
  expect(find.text('1'), findsOneWidget);
});

Finder

dart
// 通过文本查找
find.text('Hello');

// 通过类型查找
find.byType(RaisedButton);

// 通过 Key 查找
find.byKey(Key('my_button'));

// 通过图标查找
find.byIcon(Icons.add);

// 通过语义标签查找
find.bySemanticsLabel('Add');

// 通过工具方法查找
find.widgetWithText(RaisedButton, 'Click me');

完整示例

dart
// 被测试的组件
class LoginForm extends StatefulWidget {
  final Function(String, String) onSubmit;
  
  const LoginForm({Key? key, required this.onSubmit}) : super(key: key);
  
  @override
  _LoginFormState createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  
  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            controller: _emailController,
            decoration: InputDecoration(labelText: 'Email'),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please enter your email';
              }
              return null;
            },
          ),
          TextFormField(
            controller: _passwordController,
            decoration: InputDecoration(labelText: 'Password'),
            obscureText: true,
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please enter your password';
              }
              return null;
            },
          ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                widget.onSubmit(
                  _emailController.text,
                  _passwordController.text,
                );
              }
            },
            child: Text('Login'),
          ),
        ],
      ),
    );
  }
  
  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }
}

// 测试文件
void main() {
  group('LoginForm', () {
    testWidgets('should show validation errors', (tester) async {
      // 构建组件
      await tester.pumpWidget(MaterialApp(
        home: Scaffold(
          body: LoginForm(
            onSubmit: (email, password) {},
          ),
        ),
      ));
      
      // 点击登录按钮
      await tester.tap(find.text('Login'));
      await tester.pump();
      
      // 验证错误提示
      expect(
        find.text('Please enter your email'),
        findsOneWidget,
      );
      expect(
        find.text('Please enter your password'),
        findsOneWidget,
      );
    });
    
    testWidgets('should call onSubmit with form data', (tester) async {
      String? submittedEmail;
      String? submittedPassword;
      
      // 构建组件
      await tester.pumpWidget(MaterialApp(
        home: Scaffold(
          body: LoginForm(
            onSubmit: (email, password) {
              submittedEmail = email;
              submittedPassword = password;
            },
          ),
        ),
      ));
      
      // 输入表单数据
      await tester.enterText(
        find.widgetWithText(TextFormField, 'Email'),
        'test@example.com',
      );
      await tester.enterText(
        find.widgetWithText(TextFormField, 'Password'),
        'password123',
      );
      
      // 点击登录按钮
      await tester.tap(find.text('Login'));
      await tester.pump();
      
      // 验证提交的数据
      expect(submittedEmail, equals('test@example.com'));
      expect(submittedPassword, equals('password123'));
    });
  });
}

测试异步操作

dart
testWidgets('should show loading indicator', (tester) async {
  // 构建组件
  await tester.pumpWidget(MyAsyncWidget());
  
  // 验证初始状态
  expect(find.byType(CircularProgressIndicator), findsNothing);
  
  // 触发异步操作
  await tester.tap(find.byType(ElevatedButton));
  await tester.pump();
  
  // 验证加载状态
  expect(find.byType(CircularProgressIndicator), findsOneWidget);
  
  // 等待异步操作完成
  await tester.pumpAndSettle();
  
  // 验证最终状态
  expect(find.byType(CircularProgressIndicator), findsNothing);
  expect(find.text('Done'), findsOneWidget);
});

最佳实践

  1. 测试关键交互流程
  2. 验证边界条件
  3. 模拟异步操作
  4. 测试错误处理
  5. 保持测试简单

注意事项

  1. 等待动画完成
  2. 处理异步操作
  3. 清理测试资源
  4. 避免过度测试
  5. 维护测试代码

总结

Widget 测试是确保 UI 组件正确性的重要手段。通过编写全面的 Widget 测试,可以提高应用的可靠性和可维护性。