Valentin Vignal
Valentin Vignal

Reputation: 8172

Flutter Test with easy_localization and big translation json file

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?

Upvotes: 3

Views: 4205

Answers (3)

Damian K. Bast
Damian K. Bast

Reputation: 1204

When you are working with CodegenLoader this is a small method you can use to setup the easy localization singleton similar to the json example by Valentin Vignal, I would also suggest using small functions without callback parameters instead of nesting your setup functions, at least if you are not operating on the callback itself. This way it is clear that (only!) a side effect is happening.

void addLocalization() {
  Localization.load(
    const Locale('en'),
    translations: Translations(CodegenLoader.en),
  );
}

Upvotes: 0

Valentin Vignal
Valentin Vignal

Reputation: 8172

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();
}

Upvotes: 6

Gpack
Gpack

Reputation: 2193

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.

Upvotes: 1

Related Questions