作为初学者,我需要帮助编写我的第一个小部件测试。我有一个使用 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.
请帮忙!