You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 15:25:55