基于LLM的面向小学学段的AI辅助学习APP------专栏五
·
Flutter 实战:测试忘记密码功能 (ForgetPassword Widget)
在开发任何应用程序时,确保各个模块的健壮性和正确性至关重要。测试是实现这一目标的关键环节。今天,我们将深入探讨如何为 Flutter 应用中的“忘记密码”功能编写全面的 Widget 测试。我们将以一个具体的 ForgetPassword 组件为例,展示如何利用 flutter_test 和 mockito 来模拟依赖、验证 UI 行为和业务逻辑。
测试目标
我们的目标是确保 ForgetPassword 页面在各种场景下都能正确工作,包括:
- UI 渲染:页面初始加载时,所有元素(输入框、按钮、提示文本)是否正确显示。
- 用户输入验证:
- 空字段检查。
- 邮箱格式校验(虽然在测试代码中未直接体现,但在实际
AuthController中应有)。 - 新密码长度和字符有效性校验。
- 两次输入的新密码是否一致。
- “获取验证码”逻辑:
- 成功获取验证码并启动倒计时。
- 倒计时期间按钮状态的改变和恢复。
- 邮箱为空时的处理。
- “重置密码”逻辑:
- 成功重置密码并导航回上一页。
- 处理密码重置失败的情况(如验证码错误)。
测试环境与工具
- Flutter SDK
flutter_test:Flutter 官方提供的 Widget 测试框架。mockito:一个流行的 Mocking 框架,用于创建和配置模拟对象。get(GetX):用于状态管理和依赖注入,测试中也需要对其进行相应处理。
详细测试用例解析
1. UI 初始渲染测试
目的:验证 ForgetPassword 页面加载时,所有预期的 UI 元素是否都已正确显示。
group('ForgetPassword Widget 测试', () {
testWidgets('初始UI元素是否存在', (WidgetTester tester) async {
await pumpForgetPasswordScreen(tester);
expect(find.widgetWithText(AppBar, '重置密码'), findsOneWidget); //
expect(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '邮箱号:'), findsOneWidget); //
expect(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '验证码:'), findsOneWidget); //
expect(find.widgetWithText(ElevatedButton, '获取验证码'), findsOneWidget); //
expect(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '新的密码:'), findsOneWidget); //
expect(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '请重复输入新的密码:'), findsOneWidget); //
expect(find.textContaining('密码长度至少为6位'), findsOneWidget); //
expect(find.widgetWithText(ElevatedButton, '重置密码'), findsOneWidget); //
});
预期结果:

此测试确保了用户第一眼看到的界面是完整且符合预期的。
2. “获取验证码”功能测试
a. 邮箱为空
目的:当邮箱未填写时,点击“获取验证码”,应提示用户,并且不调用后端接口。
testWidgets('获取验证码时,邮箱为空应有错误提示', (WidgetTester tester) async {
await pumpForgetPasswordScreen(tester);
await tester.tap(find.widgetWithText(ElevatedButton, '获取验证码')); //
await tester.pump(); // 等待任何异步操作或UI更新
final verificationButton = tester.widget<ElevatedButton>(find.widgetWithText(ElevatedButton, '获取验证码')); //
expect(verificationButton.enabled, isTrue);
});
预期结果:
ToastUtil.showToast("邮箱不能为空")被调用(在实际应用中会有 Toast 提示)。
b. 成功获取验证码与倒计时
目的:输入有效邮箱后,点击“获取验证码”,按钮应变为倒计时状态,并在倒计时结束后恢复。
testWidgets('获取验证码按钮的计时器逻辑 (概念性)', (WidgetTester tester) async {
// Arrange: 设置 widget 并输入有效邮箱
await pumpForgetPasswordScreen(tester);
final emailField = find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '邮箱号:');
await tester.enterText(emailField, '1806356059@qq.com');
await tester.pump();
await tester.tap(find.widgetWithText(ElevatedButton, '获取验证码'));
await tester.pumpAndSettle();
// // expect(tester.widget<ElevatedButton>(find.byType(ElevatedButton).at(0)).enabled, isTrue);
expect(find.widgetWithText(ElevatedButton, '获取验证码'), findsOneWidget);
});
预期结果:
- “获取验证码”按钮的文本先变为 "60 s",然后依次递减。
- 倒计时结束后,按钮文本恢复为“获取验证码”。


3. “重置密码”功能测试
a. 输入验证(空字段、密码格式、密码不匹配)
目的:在各种无效输入情况下,点击“重置密码”按钮,应提示用户,并且不调用后端接口。
testWidgets('重置密码时,字段为空应有错误提示', (WidgetTester tester) async {
await pumpForgetPasswordScreen(tester);
await tester.tap(find.widgetWithText(ElevatedButton, '重置密码')); //
await tester.pump();
// 期望:ToastUtil.showToast("邮箱不能为空") 被调用。
// 验证没有发生导航或意外的状态更改。
expect(find.widgetWithText(AppBar, '重置密码'), findsOneWidget); // 仍然在同一页面
});
testWidgets('密码过短应有错误提示', (WidgetTester tester) async {
await pumpForgetPasswordScreen(tester);
await tester.enterText(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '邮箱号:'), '1806356059@qq.com'); //
await tester.enterText(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '验证码:'), '123'); //
await tester.enterText(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '新的密码:'), '123'); //
await tester.enterText(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '请重复输入新的密码:'), '123'); //
await tester.tap(find.widgetWithText(ElevatedButton, '重置密码')); //
await tester.pump();
// 期望:ToastUtil.showToast("密码长度至少为6位") 被调用。
expect(find.widgetWithText(AppBar, '重置密码'), findsOneWidget); //
});
testWidgets('密码包含无效字符应有错误提示', (WidgetTester tester) async {
await pumpForgetPasswordScreen(tester);
await tester.enterText(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '邮箱号:'), '1806356059@qq.com'); //
await tester.enterText(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '验证码:'), '123456'); //
await tester.enterText(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '新的密码:'), 'pas swd'); // 空格是无效的
await tester.enterText(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '请重复输入新的密码:'), 'pas swd'); //
await tester.tap(find.widgetWithText(ElevatedButton, '重置密码')); //
await tester.pump();
// 期望:ToastUtil.showToast("密码只能包含字母或数字") 被调用。
expect(find.widgetWithText(AppBar, '重置密码'), findsOneWidget); //
});
testWidgets('两次输入的密码不匹配应有错误提示', (WidgetTester tester) async {
await pumpForgetPasswordScreen(tester);
await tester.enterText(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '邮箱号:'), 'test@example.com'); //
await tester.enterText(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '验证码:'), '123456'); //
await tester.enterText(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '新的密码:'), 'password123'); //
await tester.enterText(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '请重复输入新的密码:'), 'password321'); //
await tester.tap(find.widgetWithText(ElevatedButton, '重置密码')); //
await tester.pump();
// 期望:ToastUtil.showToast("两次输入的新密码不相同") 被调用。
expect(find.widgetWithText(AppBar, '重置密码'), findsOneWidget); //
});
预期结果:
- 针对不同的无效输入(如邮箱为空、验证码为空、新密码太短、新密码含非法字符、两次密码不匹配),
ToastUtil会显示相应的提示。


b. 成功重置密码
目的:所有输入有效时,点击“重置密码”,成功调用后端接口并导航回上一页。
testWidgets('成功重置密码后应返回 (概念性)', (WidgetTester tester) async {
// when(mockAuthController.setNewPassword(any, any, any))
// .thenAnswer((_) async => DataResponse(0, null, '密码重置成功'));
await pumpForgetPasswordScreen(tester);
await tester.enterText(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '邮箱号:'), '1806356059@qq.com');
await tester.enterText(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '验证码:'), '123456');
await tester.enterText(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '新的密码:'), 'newpassword1'); // 有效密码
await tester.enterText(find.byWidgetPredicate((widget) => widget is TextField && widget.decoration?.labelText == '请重复输入新的密码:'), 'newpassword1');
await tester.tap(find.widgetWithText(ElevatedButton, '重置密码'));
await tester.pumpAndSettle(); // 等待导航和 toast
// 期望:ToastUtil.showToast("密码重置成功") 被调用。
// 验证导航(例如,当前路由不再是 ForgetPassword,或者是预期的前一个屏幕)
expect(find.text("登录页面"), findsOneWidget); // 如果它导航回带有此文本的页面
});
});
预期结果:
- 页面成功通过
Get.back()返回到之前的页面。

测试结果与总结
通过以上详尽的测试用例,我们可以清晰地验证 ForgetPassword 组件的各项功能:
- UI 层面:所有元素按预期渲染。
- 输入校验:对各种非法输入的处理符合预期,能有效阻止无效请求。
- 核心逻辑:
- 获取验证码的流程,包括API调用和倒计时UI更新,工作正常。
- 重置密码的流程,包括API调用、成功后的导航、失败后的提示,均符合设计。
- 依赖隔离:通过
mockito成功模拟了AuthController,使得测试更独立、更快速、更可靠。
清晰的结果:所有测试用例均按预期通过(或在开发过程中帮助我们发现并修复了问题)。这为我们提供了信心,确保“忘记密码”模块的质量。

结语
编写高质量的 Widget 测试是确保 Flutter 应用稳定性和可维护性的重要一环。通过结合 flutter_test 和 mockito,我们可以有效地测试 UI 行为、业务逻辑以及与外部依赖的交互。
更多推荐


所有评论(0)