Flutter测试无法找到自定义组件中的TextFormField问题排查
问题描述
使用flutter_test库测试Login组件时,出现错误:Found 0 widgets with type "TextFormField": [],但Login组件实际包含两个TextFormField,推测组件未正确挂载,求修改方案。
测试代码
import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_dnd/screens/login.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; void main() { testWidgets('Verify that we will got an error on wrong email input', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp(home: Login())); var textField = find.byType(TextFormField); expect(textField, findsNWidgets(2)); } ); }
Login组件代码
import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; import '../firebase_options.dart'; import '../utils/fireAuth.dart'; import '../utils/validators.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_dnd/providers/authProvider.dart'; class Login extends ConsumerStatefulWidget { const Login({super.key}); @override ConsumerState<Login> createState() => _LoginState(); } class _LoginState extends ConsumerState<Login> { String email = ''; String password = ''; Future<FirebaseApp?>? firebaseInit; final _formKey = GlobalKey<FormState>(); @override void initState() { super.initState(); firebaseInit = _initializeFirebase(); } Future<FirebaseApp?> _initializeFirebase() async { try { FirebaseApp firebaseApp = await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); return firebaseApp; } catch (e) { print('ERROR ${e}'); return null; } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: const Text('Login'), ), body: FutureBuilder( future: firebaseInit, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { return Center( child: Form( key: _formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ TextFormField( key: const ValueKey('email'), decoration: InputDecoration( hintText: "Email", errorBorder: UnderlineInputBorder( borderRadius: BorderRadius.circular(6.0), borderSide: const BorderSide( color: Colors.red, ), ), ), validator: (value) => Validator.validateEmail( email: value, ), onChanged: (value) { setState(() { email = value; }); }, ), const Padding(padding: EdgeInsets.symmetric(vertical: 16)), TextFormField( decoration: InputDecoration( hintText: "Password", errorBorder: UnderlineInputBorder( borderRadius: BorderRadius.circular(6.0), borderSide: const BorderSide( color: Colors.red, ), ), ), validator: (value) => Validator.validatePassword( password: value, ), onChanged: (String value) { setState(() { password = value; }); }, ), const Padding(padding: EdgeInsets.symmetric(vertical: 16)), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { void callback(bool value) => ref.read(isAuthenticated.notifier).update((state) => value); FireAuth.signInWithEmail(email: email, password: password, callback: callback); } }, child: const Text('Login') ), TextButton( onPressed: () { Navigator.pushNamed(context, '/signUp'); }, child: const Text('Sign up')) ], ) ) ], ), ) ); } return const Center( child: CircularProgressIndicator(), ); } ) ); } }
main.dart代码
import 'package:flutter/material.dart'; import 'package:flutter_dnd/screens/login.dart'; import 'package:flutter_dnd/screens/signup.dart'; import 'package:flutter_dnd/screens/home.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_dnd/providers/authProvider.dart'; Future<void> main() async { await dotenv.load(); runApp(ProviderScope( child: MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), initialRoute: '/', routes: { '/': (context) => const MainScreen(), '/signUp': (context) => const SignUp(), }, ), )); } class MainScreen extends ConsumerWidget { const MainScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final authState = ref.watch(isAuthenticated); if (authState) { return const GameChat(); } return const Login(); } }
解决方案
1. 等待FutureBuilder完成异步渲染
Login组件的核心UI被包裹在FutureBuilder中,依赖Firebase初始化的异步任务。原测试代码仅调用pumpWidget,此时界面还在显示CircularProgressIndicator,未渲染出TextFormField。
修改测试代码:
- 使用
pumpAndSettle()替代pumpWidget(),它会等待所有异步任务和帧渲染完成 - 同时给Login包裹
ProviderScope,因为它是ConsumerStatefulWidget,需要Riverpod上下文
修改后的测试代码:
import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_dnd/screens/login.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; void main() { testWidgets('Verify that we will got an error on wrong email input', (WidgetTester tester) async { // 用ProviderScope包裹Login,提供Riverpod上下文 await tester.pumpWidget(ProviderScope(child: MaterialApp(home: Login()))); // 等待FutureBuilder完成Firebase初始化并渲染完成 await tester.pumpAndSettle(); var textField = find.byType(TextFormField); expect(textField, findsNWidgets(2)); } ); }
2. 可选:Mock Firebase初始化(避免依赖真实环境)
如果不想让测试依赖真实Firebase服务,可以mock初始化方法,消除异步延迟或初始化失败的影响:
步骤1:添加mockito依赖
在pubspec.yaml的dev_dependencies中添加:
dev_dependencies: flutter_test: sdk: flutter mockito: ^5.4.0
步骤2:修改测试代码添加mock
import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_dnd/screens/login.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:mockito/mockito.dart'; // 定义Mock类 class MockFirebaseApp extends Mock implements FirebaseApp {} void main() { setUp(() { // Mock Firebase初始化方法 when(Firebase.initializeApp(options: anyNamed('options'))) .thenAnswer((_) async => MockFirebaseApp()); }); testWidgets('Verify that we will got an error on wrong email input', (WidgetTester tester) async { await tester.pumpWidget(ProviderScope(child: MaterialApp(home: Login()))); await tester.pumpAndSettle(); var textField = find.byType(TextFormField); expect(textField, findsNWidgets(2)); } ); }
内容的提问来源于stack exchange,提问作者Stas Motorny
相关产品推荐
相关产品推荐

