Flutter Test with easy_localization and big translation json file

681

Solution 1

In the end, I fixed it by adding a file test/flutter_test_config.dart. I got the inspiration from this issue.

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:easy_localization/src/localization.dart';
import 'package:easy_localization/src/translations.dart';
import 'package:flutter/widgets.dart';

Future<void> testExecutable(FutureOr<void> Function() testMain) async {
  final content = await File('assets/lang/sv.json').readAsString(); // <- Or `ru.json`
  final data = jsonDecode(content) as Map<String, dynamic>;

  // easy_localization works with a singleton instance internally. We abuse
  // this fact in tests and just let it load the English translations.
  // Therefore we don't need to deal with any wrapper widgets and
  // waiting/pumping in our widget tests.
  Localization.load(
    const Locale('en'),
    translations: Translations(data),
  );


  await testMain();
}

Solution 2

This is because of how the json language file is loaded.
Take a look at this:

// from flutter/lib/src/services/asset_bundle.dart

Future<String> loadString(String key, { bool cache = true }) async {
    final ByteData data = await load(key);
    // Note: data has a non-nullable type, but might be null when running with
    // weak checking, so we need to null check it anyway (and ignore the warning
    // that the null-handling logic is dead code).
    if (data == null)
      throw FlutterError('Unable to load asset: $key'); // ignore: dead_code
    // 50 KB of data should take 2-3 ms to parse on a Moto G4, and about 400 μs
    // on a Pixel 4.
    if (data.lengthInBytes < 50 * 1024) {
      return utf8.decode(data.buffer.asUint8List());
    }
    // For strings larger than 50 KB, run the computation in an isolate to
    // avoid causing main thread jank.
    return compute(_utf8decode, data, debugLabel: 'UTF8 decode for "$key"');
}

If the file is bigger than a certain size, Flutter uses compute to load it and avoid UI jank.

If in your test, you add this line before the await tester.pumpAndSettle(), this will pause the main thread, give some time for the isolate to complete and resume execution.

await Future.delayed(const Duration(milliseconds: 150), () {}); // this line
await tester.pumpAndSettle()

On my laptop 150ms was the minimum duration to have the test pass consistently.

Share:
681
Valentin Vignal
Author by

Valentin Vignal

Student in CentralSupélec and in the National University of Singapore

Updated on December 31, 2022

Comments

  • Valentin Vignal
    Valentin Vignal over 1 year

    I'm using easy_localization in a flutter project. I need to write some widget tests.

    But it looks like that when the .json file with the translations is too big, the MaterialApp never builds its child and therefore, I cannot test my widgets.

    Here is the architecture of my small reproducible project:

    my_project
     |- assets
     |   |- lang
     |   |   |- ru.json
     |   |   |- sv.json
     |- test
     |   |- test_test.dart
    

    Here is my test_test.dart file:

    import 'package:easy_localization/easy_localization.dart';
    import 'package:easy_logger/easy_logger.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_test/flutter_test.dart';
    import 'package:shared_preferences/shared_preferences.dart';
    
    void main() {
      testWidgets('', (tester) async {
        await tester.runAsync(() async {
          SharedPreferences.setMockInitialValues({});
          EasyLocalization.logger.enableLevels = <LevelMessages>[
            LevelMessages.error,
            LevelMessages.warning,
          ];
          await EasyLocalization.ensureInitialized();
          await tester.pumpWidget(
            EasyLocalization(
              supportedLocales: const [Locale('sv')],  // <- Change it to 'ru' and it doesn't work
              path: 'assets/lang',
              child: Builder(
                builder: (context) {
                  print('builder1');
                  return MaterialApp(
                    locale: EasyLocalization.of(context).locale,
                    supportedLocales: EasyLocalization.of(context).supportedLocales,
                    localizationsDelegates: EasyLocalization.of(context).delegates,
                    home: Builder(
                      builder: (context) {
                        print('builder2');
                        return const SizedBox.shrink();
                      },
                    ),
                  );
                },
              ),
            ),
          );
          await tester.pumpAndSettle();
        });
      });
    }
    

    sv.json (a small file):

    {
      "0": "0"
    }
    

    ru.json (a big file):

    {
      "0": "0 - xx ... xx",  // <- 1000 "x"
      "1": "1 - xx ... xx",  // <- 1000 "x"
      // ...
      "3998": "3998 - xx ... xx",  // <- 1000 "x"
      "3999": "3999 - xx ... xx"  // <- 1000 "x"
    }
    

    In my test, I have 2 prints which should respectively print builder1 and builder2.

    This works well when I use Locale('sv') in my test:

    00:02 +0:                                                                                                                                                                                                                                               
    builder1
    builder2
    00:02 +1: All tests passed!
    

    But when I use Locale('ru'), MaterialApp doesn't build its child and I don't get the print builder2:

    00:03 +0:                                                                                                                                                                                                                                               
    builder1
    00:03 +1: All tests passed!
    

    How can I fix this?