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

Flutter自定义LocalizationDelegate导致Widget测试失败问题排查

问题描述

自定义了处理应用本地化的LocalizationDelegate,代码如下:

class LocalizationDelegate extends LocalizationsDelegate<Localization> {
  const LocalizationDelegate();

  @override
  bool isSupported(Locale locale) => ['en', 'fr'].contains(locale.languageCode);

  @override
  Future<Localization> load(Locale locale) async {
    String string = await rootBundle
        .loadString("assets/strings/${locale.languageCode}.json");
    language = json.decode(string);
    return SynchronousFuture<Localization>(Localization());
  }

  @override
  bool shouldReload(LocalizationDelegate old) => false;
}

运行Widget测试时抛出错误,测试代码:

testWidgets('Localization test', (WidgetTester tester) async {
    await tester.pumpWidget(const FakeTestPage());
    expect(find.text('TEST'), findsOneWidget);
    print("Done !");
  });

FakeTestPage代码:

class FakeTestPage extends StatelessWidget {
  const FakeTestPage({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      localizationsDelegates: [
        LocalizationDelegate(),
        GlobalCupertinoLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: [Locale("fr"), Locale("en")],
      locale: Locale("fr"),
      home: Scaffold(
        body: Text("TEST"),
      ),
    );
  }
}

错误日志:

/Users/foxtom/Desktop/flutter/bin/flutter --no-color test --machine --start-paused test/localization_test.dart
Testing started at 11:07 ...

══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown running a test:
Expected: exactly one matching node in the widget tree
  Actual: _TextFinder:<zero widgets with text "TEST" (ignoring offstage widgets)>
   Which: means none were found but one was expected

When the exception was thrown, this was the stack:
#4      main.<anonymous closure> (file:///Users/foxtom/StudioProjects/TestProject/test/localization_test.dart:12:5)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)

This was caught by the test expectation on the following line:
  file:///Users/foxtom/StudioProjects/TestProject/test/localization_test.dart line 12
The test description was:
  Localization test
════════════════════════════════════════════════════════════════════════════════════════════════════

Test failed. See exception logs above.
The test description was: Localization test

移除localizationsDelegates中的LocalizationDelegate()后测试可正常运行,疑问:是否是Delegate的Future部分存在问题?如何让测试与实际应用一样正常运行?


问题原因与解决方法

核心原因

测试失败的关键有两点:

  1. 测试环境未加载assets资源:默认情况下Flutter测试不会自动加载assets目录下的文件,rootBundle.loadString会执行失败,导致本地化初始化出错,阻塞了MaterialApp的渲染流程,最终目标Text("TEST")未被正确渲染。
  2. 异步逻辑未被正确等待:虽然load方法最后返回了SynchronousFuture,但之前的await rootBundle.loadString是真实异步操作,测试中没有等待资源加载完成就执行了断言。

解决步骤

步骤1:配置测试环境加载assets

在测试文件中添加初始化逻辑,注册并预加载所需的本地化资源:

import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  setUp(() async {
    // 确保测试绑定初始化
    await TestWidgetsFlutterBinding.ensureInitialized();
    // 预加载测试用到的本地化文件
    await rootBundle.loadString('assets/strings/fr.json');
  });

  testWidgets('Localization test', (WidgetTester tester) async {
    await tester.pumpWidget(const FakeTestPage());
    // 等待异步操作完成并触发UI重绘
    await tester.pumpAndSettle();
    expect(find.text('TEST'), findsOneWidget);
    print("Done !");
  });
}

步骤2:给LocalizationDelegate添加容错处理

在load方法中捕获资源加载异常,避免崩溃阻塞UI渲染:

@override
Future<Localization> load(Locale locale) async {
  try {
    String string = await rootBundle
        .loadString("assets/strings/${locale.languageCode}.json");
    language = json.decode(string);
  } catch (e) {
    // 加载失败时使用空字典或默认值
    language = {};
    print("本地化文件加载失败: $e");
  }
  return SynchronousFuture<Localization>(Localization());
}

步骤3:确认测试用本地化文件存在

确保assets/strings/fr.json文件在项目中存在且格式正确,测试环境需要能正常读取该文件。

可选优化:Mock本地化Delegate

如果只是测试UI而非本地化逻辑,可以Mock一个跳过真实资源加载的Delegate,让测试更高效稳定:

class MockLocalizationDelegate extends LocalizationsDelegate<Localization> {
  const MockLocalizationDelegate();

  @override
  bool isSupported(Locale locale) => true;

  @override
  Future<Localization> load(Locale locale) async {
    language = {};
    return SynchronousFuture<Localization>(Localization());
  }

  @override
  bool shouldReload(MockLocalizationDelegate old) => false;
}

然后在FakeTestPage的localizationsDelegates中替换为MockLocalizationDelegate()即可。


内容的提问来源于stack exchange,提问作者Tom3652

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 19:02:07