跳转到正文
YGJoon
返回

Flutter Widget 测试详解

编辑文章

简介

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

基本配置

添加依赖

dev_dependencies:
  flutter_test:
    sdk: flutter

创建测试文件

// 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

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

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

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

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

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

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

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

完整示例

// 被测试的组件
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'),
        '[email protected]',
      );
      await tester.enterText(
        find.widgetWithText(TextFormField, 'Password'),
        'password123',
      );
      
      // 点击登录按钮
      await tester.tap(find.text('Login'));
      await tester.pump();
      
      // 验证提交的数据
      expect(submittedEmail, equals('[email protected]'));
      expect(submittedPassword, equals('password123'));
    });
  });
}

测试异步操作

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 测试,可以提高应用的可靠性和可维护性。


编辑文章
分享这篇文章:

上一篇
Flutter 单元测试详解
下一篇
Flutter 渲染优化详解