-2

作为初学者,我需要帮助编写我的第一个小部件测试。我有一个使用 firebase auth 的登录页面,并想测试登录流程。我的第一个测试是确保在没有提供电子邮件和密码的情况下不按下登录按钮。但是我经常出错,我在网上看到并尝试过的每一个建议都对我没有用。我的代码如下。

登录屏幕

class LoginScreen extends StatefulWidget {
  const LoginScreen({Key? key}) : super(key: key);

  @override
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final formKey = GlobalKey<FormBuilderState>();
  bool isObscure = true;
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: BlocConsumer<AuthCubit, AuthState>(
          listener: (context, state) {
            if (state is AuthLoginError) {
              key:
              const Key("snack_bar_failure");
              ScaffoldMessenger.of(context)
                ..hideCurrentSnackBar()
                ..showSnackBar(SnackBar(
                  content: Text(state.errorMessage!),
                  backgroundColor: Colors.red,
                ));
            }
    
            if (state is AuthLoginSuccess) {
              formKey.currentState!.reset();
              Navigator.pushNamedAndRemoveUntil(
                  context, AppRoutes.home, (r) => false);
            }
          },
          builder: (context, state) => _buildLoginScreen(),
        ),
      ),
    );
  }

  Widget _buildLoginScreen() {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      body: FormBuilder(
        key: formKey,
        autovalidateMode: AutovalidateMode.disabled,
        child: SingleChildScrollView(
          child: Padding(
            padding: const EdgeInsets.fromLTRB(12.0, 40.0, 12.0, 12.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Align(
                  alignment: Alignment.centerLeft,
                  child: BackButton(
                    onPressed: () => Navigator.pop(context),
                  ),
                ),
                const Text(
                  'Login',
                  style: TextStyle(
                    fontSize: 28,
                    color: Colors.blueAccent,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(
                  height: 20,
                ),
                SizedBox(
                    width: MediaQuery.of(context).size.width * 0.9,
                    child: FormBuilderTextField(
                      name: 'email',
                      validator: FormBuilderValidators.compose([
                        FormBuilderValidators.email(context,
                            errorText: "Email cannot be empty"),
                      ]),
                      textInputAction: TextInputAction.next,
                      decoration: InputDecoration(
                        contentPadding: const EdgeInsets.all(8.0),
                        prefixIcon: const Icon(Icons.email),
                        hintText: "Email",
                        hintStyle: kHintStyle,
                        filled: true,
                        fillColor: Colors.grey[200],
                        enabledBorder: kOutlineBorder,
                        focusedBorder: kOutlineBorder,
                        errorBorder: kOutlineErrorBorder,
                        focusedErrorBorder: kOutlineErrorBorder,
                      ),
                    ),),
                const SizedBox(
                  height: 15,
                ),
                SizedBox(
                  width: MediaQuery.of(context).size.width * 0.9,
                  child: FormBuilderTextField(
                    name: "password",
                    obscureText: isObscure,
                    textInputAction: TextInputAction.next,
                    decoration: InputDecoration(
                        contentPadding: const EdgeInsets.all(8.0),
                        prefixIcon: const Icon(Icons.lock),
                        hintText: "Password",
                        hintStyle: kHintStyle,
                        filled: true,
                        fillColor: Colors.grey[200],
                        enabledBorder: kOutlineBorder,
                        focusedBorder: kOutlineBorder,
                        errorBorder: kOutlineErrorBorder,
                        focusedErrorBorder: kOutlineErrorBorder,
                        suffixIcon: InkWell(
                          onTap: () {
                            setState(() {
                              isObscure = !isObscure;
                            });
                          },
                          child: Icon(
                            isObscure
                                ? Icons.radio_button_off
                                : Icons.radio_button_checked,
                          ),
                        )),
                  ),
                ),
                const SizedBox(height: 25),
                LoginButton(
                    key: const Key("login"),
                    onPressed: () async {
                      if (formKey.currentState!.validate() &&
                          formKey.currentState!.fields['email']!.value !=
                              null &&
                          formKey.currentState!.fields['password']!.value !=
                              null) {
                        final authCubit = BlocProvider.of<AuthCubit>(context);
                        await authCubit.login(
                            formKey.currentState!.fields['email']!.value.trim(),
                            formKey.currentState!.fields['password']!.value);
                      } else if (formKey.currentState!.fields['email']!.value ==
                              null ||
                          formKey.currentState!.fields['password']!.value ==
                              null) {
                        ScaffoldMessenger.of(context)
                          ..hideCurrentSnackBar()
                          ..showSnackBar(const SnackBar(
                            content: Text(
                                "Email and Password fields cannot be empty!"),
                            backgroundColor: Colors.red,
                          ));
                      }
                    }),
                TextButton(
                    onPressed: () =>
                        Navigator.pushNamed(context, AppRoutes.forgotPassword),
                    child: const Text("Forgot Password?")),
                const Divider(
                  height: 20,
                  endIndent: 10,
                  indent: 8,
                ),
                const SizedBox(
                  height: 20,
                ),
                const SizedBox(
                  height: 20,
                  child: Text(
                    "Don't have an Account?",
                    style: TextStyle(color: Colors.blueGrey),
                  ),
                ),
                const SizedBox(
                  height: 20,
                ),
                CustomButton(
                    child: const Text("Create An Account"),
                    width: MediaQuery.of(context).size.width * 0.9,
                    height: MediaQuery.of(context).size.height * 0.05,
                    onPressed: () =>
                        Navigator.pushNamed(context, AppRoutes.signup))
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class LoginButton extends StatelessWidget {
  final Function onPressed;
  const LoginButton({Key? key, required this.onPressed}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CustomButton(
        height: MediaQuery.of(context).size.height * 0.05,
        width: MediaQuery.of(context).size.width * 0.9,
        onPressed: onPressed,
        child: BlocConsumer<AuthCubit, AuthState>(
          listener: (context, state) {
            // TODO: implement listener
          },
          builder: (context, state) {
            if (state is AuthLoginLoading) {
              return kLoaderBtn;
            } else {
              return const Text("Login");
            }
          },
        ));
  }
}



测试文件

class MockAuthCubit extends Mock implements AuthCubit {}

void main() {

  Widget makeTestableWidget({required Widget child, required AuthCubit auth}) {
    return MultiBlocProvider(
      providers: [
        BlocProvider(
          create: (context) => auth,
        ),
      ],
      child: MaterialApp(
        home: Scaffold(
          body: child,
        ),
      ),
    );
  }

  testWidgets(
      'login screen, with empty email and password, expects firebase login not to occur',
      (WidgetTester tester) async {
    LoginScreen screen = const LoginScreen();

    MockAuthCubit mockAuth = MockAuthCubit();

    await tester.pumpWidget(makeTestableWidget(child: screen, auth: mockAuth));

    final Finder loginButton = find.byKey(const Key('login'));

    await tester.tap(loginButton);

    verifyNever(mockAuth.login('', ''));
  });
}

错误

══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following _TypeError was thrown building _BodyBuilder:
type 'Null' is not a subtype of type 'Stream<AuthState>'

The relevant error-causing widget was:
  Scaffold
  Scaffold:file:.../test/login_screen_test.dart:20:15

When the exception was thrown, this was the stack:
#0      MockAuthCubit.stream (package:bloc/src/bloc_base.dart:71:21)
#1      BlocProvider._startListening (package:flutter_bloc/src/bloc_provider.dart:143:32)
#2      _CreateInheritedProviderState.value (package:provider/src/inherited_provider.dart:779:50)
#3      _InheritedProviderScopeElement.value (package:provider/src/inherited_provider.dart:583:33)
#4      Provider.of (package:provider/src/provider.dart:303:37)
#5      ReadContext.read (package:provider/src/provider.dart:656:21)
#6      _BlocConsumerState.initState (package:flutter_bloc/src/bloc_consumer.dart:108:36)
#7      StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:4893:57)
#8      ComponentElement.mount (package:flutter/src/widgets/framework.dart:4729:5)
...     Normal element mounting (19 frames)
#27     Element.inflateWidget (package:flutter/src/widgets/framework.dart:3790:14)
#28     MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:6422:36)
#29     MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6433:32)
...     Normal element mounting (255 frames)
#284    Element.inflateWidget (package:flutter/src/widgets/framework.dart:3790:14)
#285    MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:6422:36)
#286    MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6433:32)
...     Normal element mounting (371 frames)
#657    Element.inflateWidget (package:flutter/src/widgets/framework.dart:3790:14)
#658    Element.updateChild (package:flutter/src/widgets/framework.dart:3540:18)
#659    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4780:16)
#660    _InheritedProviderScopeElement.performRebuild (package:provider/src/inherited_provider.dart:495:11)
#661    Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#662    ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4735:5)
#663    ComponentElement.mount (package:flutter/src/widgets/framework.dart:4729:5)
#664    _InheritedProviderScopeElement.mount (package:provider/src/inherited_provider.dart:395:11)
...     Normal element mounting (7 frames)
#671    SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11)
...     Normal element mounting (7 frames)
#678    SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11)
...     Normal element mounting (7 frames)
#685    _NestedHookElement.mount (package:nested/nested.dart:187:11)
...     Normal element mounting (7 frames)
#692    SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11)
#693    Element.inflateWidget (package:flutter/src/widgets/framework.dart:3790:14)
#694    Element.updateChild (package:flutter/src/widgets/framework.dart:3524:20)
#695    RenderObjectToWidgetElement._rebuild (package:flutter/src/widgets/binding.dart:1198:16)
#696    RenderObjectToWidgetElement.update (package:flutter/src/widgets/binding.dart:1175:5)
#697    RenderObjectToWidgetElement.performRebuild (package:flutter/src/widgets/binding.dart:1189:7)
#698    Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#699    BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2659:19)
#700    AutomatedTestWidgetsFlutterBinding.drawFrame (package:flutter_test/src/binding.dart:1128:19)
#701    RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:363:5)
#702    SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1144:15)
#703    SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1081:9)
#704    AutomatedTestWidgetsFlutterBinding.pump.<anonymous closure> (package:flutter_test/src/binding.dart:995:9)
#707    TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#708    AutomatedTestWidgetsFlutterBinding.pump (package:flutter_test/src/binding.dart:982:27)
#709    WidgetTester.pumpWidget.<anonymous closure> (package:flutter_test/src/widget_tester.dart:548:22)
#712    TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#713    WidgetTester.pumpWidget (package:flutter_test/src/widget_tester.dart:545:27)
#714    main.<anonymous closure> (file:.../test/login_screen_test.dart:37:18)
#715    main.<anonymous closure> (file:.../test/login_screen_test.dart:32:7)
#716    testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:170:29)
<asynchronous suspension>
<asynchronous suspension>
(elided 5 frames from dart:async and package:stack_trace)

════════════════════════════════════════════════════════════════════════════════════════════════════
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown running a test:
The finder "zero widgets with key [<'login'>] (ignoring offstage widgets)" (used in a call to
"tap()") could not find any matching widgets.

When the exception was thrown, this was the stack:
#0      WidgetController._getElementPoint (package:flutter_test/src/controller.dart:900:7)
#1      WidgetController.getCenter (package:flutter_test/src/controller.dart:839:12)
#2      WidgetController.tap (package:flutter_test/src/controller.dart:273:18)
#3      main.<anonymous closure> (...test/login_screen_test.dart:41:18)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)

The test description was:
  login screen, with empty email and password, expects firebase login not to occur
════════════════════════════════════════════════════════════════════════════════════════════════════
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following message was thrown:
Multiple exceptions (2) were detected during the running of the current test, and at least one was
unexpected.
════════════════════════════════════════════════════════════════════════════════════════════════════
00:02 +0 -1: login screen, with empty email and password, expects firebase login not to occur [E]                                           
  Test failed. See exception logs above.
  The test description was: login screen, with empty email and password, expects firebase login not to occur
  
00:02 +0 -1: Some tests failed.                                                                                                             

请帮忙!

4

0 回答 0