Flutter集成测试组中第二个testWidgets调用失败,单独运行正常
场景
我有一个简单的Flutter应用,包含3个页面:LoginPage、CheckoutPage和DashboardPage。LoginPage有两个按钮:一个用于跳转至CheckoutPage(键为ValueKey('Subscribe')),另一个用于跳转至DashboardPage(键为ValueKey('Login'))。
代码
我的integration_test/main_test.dart代码如下:
void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('end to end test', () { // setUp(() async { // app.main(); // }); testWidgets( 'Given textfields for Email and Password, When correct values are entered, Then user is taken to their DashboardPage', (tester) async { // Arrange app.main(); await tester.pumpAndSettle(); await Future.delayed(const Duration(seconds: 2)); // Act Finder emailTextField = find.byKey(const ValueKey('EmailAddress')); Finder passwordTextField = find.byKey(const ValueKey('Password')); Finder loginButton = find.byKey(const ValueKey('Login')); Finder dashboardPage = find.byType(DashboardPage); // Assert expect(emailTextField, findsOneWidget); expect(passwordTextField, findsOneWidget); expect(loginButton, findsOneWidget); await tester.enterText(emailTextField, 'ayan@qolarisdata.com'); await Future.delayed(const Duration(seconds: 2)); await tester.enterText(passwordTextField, 'Test@123'); await Future.delayed(const Duration(seconds: 4)); await tester.tap(loginButton); await tester.pumpAndSettle(); expect(dashboardPage, findsOneWidget); await Future.delayed(const Duration(seconds: 5)); }); testWidgets( 'Given a Subscribe button, When the button is tapped on, Then the user is taken to Checkout screen', (tester) async { app.main(); // await tester.pumpWidget(const app.MainApp()); await tester.pumpAndSettle(); await Future.delayed(const Duration(seconds: 2)); expect(find.byKey(const ValueKey('Subscribe')), findsOneWidget); await tester.tap(find.byKey(const ValueKey('Subscribe'))); await tester.pumpAndSettle(); expect(find.byType(CheckoutPage), findsOneWidget); await Future.delayed(const Duration(seconds: 5)); }); }); }
问题
当我以组为单位运行测试时,第一个测试运行成功,第二个测试失败,报错:
The following TestFailure was thrown running a test:
Expected: exactly one matching candidate
Actual: _KeyWidgetFinder:<Found 0 widgets with key [<'Subscribe'>]: []>
但单独运行第二个测试时,它可以正常通过。我猜测在测试组中,第一个测试完成后应用停留在DashboardPage,第二个测试启动时,应用并未重新从LoginPage开始。
我尝试注释掉app.main(),替换为await tester.pumpWidget(const app.MainApp());或:
await tester.pumpWidget( MaterialApp.router( routerConfig: GoRouteInformation.router, ), );
但仍出现相同错误。
核心原因
测试组内的多个测试会共享同一个应用状态:第一个测试执行完成后,应用已经导航到DashboardPage,第二个测试启动时没有重置应用的路由和页面状态,导致找不到LoginPage上的Subscribe按钮。
另外,直接调用app.main()或者重复pumpWidget未必能正确清空路由栈,因为Flutter测试环境中,路由状态不会自动在测试间重置。
修复方案
1. 用setUp统一初始化应用
在测试组内添加setUp方法,确保每个测试开始前都重新初始化应用并等待页面加载完成,彻底重置状态:
setUp(() async { await tester.pumpWidget(const app.MainApp()); await tester.pumpAndSettle(); });
2. 移除不可靠的Future.delayed
集成测试中尽量用tester.pumpAndSettle()代替固定延迟,它会自动等待所有动画、导航等异步操作完成,测试更稳定且耗时更短。
3. 针对路由库的额外处理(如果用GoRouter等)
如果项目用了GoRouter这类路由管理库,需要在setUp中主动重置路由到初始页面:
setUp(() async { await tester.pumpWidget(const app.MainApp()); // 重置路由到登录页 app.router.go('/login'); await tester.pumpAndSettle(); });
修改后的完整代码
void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('end to end test', () { setUp(() async { // 每个测试前重置应用状态 await tester.pumpWidget(const app.MainApp()); await tester.pumpAndSettle(); }); testWidgets( '输入正确邮箱密码后跳转到DashboardPage', (tester) async { // 定位元素 final emailTextField = find.byKey(const ValueKey('EmailAddress')); final passwordTextField = find.byKey(const ValueKey('Password')); final loginButton = find.byKey(const ValueKey('Login')); final dashboardPage = find.byType(DashboardPage); // 断言初始页面元素存在 expect(emailTextField, findsOneWidget); expect(passwordTextField, findsOneWidget); expect(loginButton, findsOneWidget); // 执行操作 await tester.enterText(emailTextField, 'ayan@qolarisdata.com'); await tester.pumpAndSettle(); await tester.enterText(passwordTextField, 'Test@123'); await tester.pumpAndSettle(); await tester.tap(loginButton); await tester.pumpAndSettle(); // 断言跳转成功 expect(dashboardPage, findsOneWidget); }); testWidgets( '点击Subscribe按钮跳转到CheckoutPage', (tester) async { // 定位元素 final subscribeButton = find.byKey(const ValueKey('Subscribe')); final checkoutPage = find.byType(CheckoutPage); // 断言初始页面按钮存在 expect(subscribeButton, findsOneWidget); // 执行操作 await tester.tap(subscribeButton); await tester.pumpAndSettle(); // 断言跳转成功 expect(checkoutPage, findsOneWidget); }); }); }
内容的提问来源于stack exchange,提问作者Ayan Dasgupta

