KINTO Tech Blog
Development

Are You Unit Testing with Flutter Web?

Cover Image for Are You Unit Testing with Flutter Web?

Unit testing with Flutter Web

Hello. I am Osugi from the Woven Payment Solution Development Group.

My team is developing the payment system that will be used by Woven by Toyota for the Toyota Woven City. We mainly use Kotlin/Ktor for backend development and Flutter for the frontend.

In Flutter Web, errors in test runs can occur when using web-specific packages. Therefore, in this article, I would like to summarize what we are doing to make Flutter Web code testable, with a particular focus on unit testing.

If you're interested in reading about the story behind our frontend development journey thus far, feel free to check out this article:

What is Flutter Web?

First of all, Flutter is a cross-platform development framework developed by Google, and Flutter Web is a framework specialized for web application development.

Dart, a Flutter's development language, can convert source code to JavaScript in advance and perform drawing processes using HTML, Canvas, and CSS, allowing code developed for mobile applications to be ported directly to web applications.

How To Implement Flutter Web

Basic implementation can be done in the same way as mobile application development. On the other hand, what if you need to access DOM manipulations or browser APIs?

This is also available in Dart's built-in packages for web platforms such as dart:html[1]. For example, the file download function can be implemented in the same way as general web application development with JavaScript.

The widget below is a sample application with a function that does not know what it is used for, which is to download the counted-up numbers as a text file. The text file will be downloaded by clicking the Floating Button.

my_home_page.dart
import 'dart:html';

import 'package:flutter/material.dart';

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            IconButton(
              onPressed: _incrementCounter,
              icon: const Icon(Icons.add),
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          AnchorElement(href: 'data:text/plain;charset=utf-8,$_counter')
            ..setAttribute('download', 'counter.txt')
            ..click();
        },
        tooltip: 'Download',
        child: const Icon(Icons.download),
      ),
    );
  }
}

Unit Testing Flutter Web Code

The test code for the previous sample code is prepared as follows (mostly as it was when flutter create was done and output).

widget_test.dart

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

import 'package:sample_web/main.dart';

void main() {
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    await tester.pumpWidget(const MyApp());

    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });
}

Run the following test command or the above test code from the Testing tab of VS Code.

$ flutter test

If you run the test as it is, you will probably get an error like the following.


Error: Dart library 'dart:html' is not available on this platform.

// omitted

lib/utils/src/html_util.dart:4:3: Error: Method not found: 'AnchorElement'.
  AnchorElement(href: 'data:text/plain;charset=utf-8,$data')

Apparently, there is something wrong with importing dart:html.

Platform-Specific Dart Compiler

The official documentation indicates that running the Dart compiler requires:

  • Native platform which includes Dart VM with JIT compiler and AOT compiler for producing machine code.
  • Web platform to transpile Dart code into JavaScript.

We can see there are two platforms. In addition, some of the packages available for each platform appear to be different.

Platform Available Packages
Native dart:ffi, dart:io, dart:isolate
Web dart:html, dart:js, dart:js_interop etc.

So, it turned out that the above test was running on a VM, and therefore dart:html was not available.

Specifying the platform at test runtime is one way to avoid import errors for Web platform packages. You can specify that the test should run on Chrome (as a web) by running the command with the following options[2].

$ flutter test --platform chrome

Should Flutter Web Test Code Run on Chrome?

When developing web applications, using the browser API is inevitable, but should the Flutter Web test code be run on Chrome?

In my personal opinion, it is better to avoid using Chrome as much as possible.

The reasons are:

  • Running tests requires Chrome to be launched in the background, which increases test launch time.
  • Chrome must be installed in the CI environment, which increases the container size of the CI environment. Or it may take a long time to set up containers,

which would considerably increase the monetary cost of a CI environment. (Of course, if you just want to do a quick local check or if you are a wealthy person, no problem!)

In fact, I have included the results of running the local environment comparing the standard case (Native) with no platform specified and the case (Web) with Chrome specified.

Platform Program run time (sec) Total test run time (sec)
Native 2.0 2.5
Web 2.5 9.0

From the table above, the Web actually took significantly longer to launch the test. You will also notice that test run time has also increased by about 25%.

tester

Separate Web Platform-Dependent Code

Can the above error be avoided without specifying a web platform?

In fact, Dart also offers conditional imports and exports for packages, along with flags to determine whether the platform is Web or Native[3].

Flag Description
dart.library.html Whether a Web platform
dart.library.io Whether a Native platform

These can be used to avoid errors.

First, prepare the download function module for Web and Native as follows, and separate the aforementioned web package usage part from the code to be tested.

util_html.dart
import 'dart:html';

void download(String fileName, String data) {
  AnchorElement(href: 'data:text/plain;charset=utf-8,$data')
    ..setAttribute('download', fileName)
    ..click();
}
util_io.dart
void download(String fileName, String data) =>
    throw UnsupportedError('Not support this platform');

Here is how to switch the import of the above module for each platform.

my_home_page.dart
import 'package:flutter/material.dart';

- import 'dart:html'
+ import './utils/util_io.dart'
+     if (dart.library.html) './utils/util_html.dart';

class MyHomePage extends StatefulWidget {

  // omitted

}

class _MyHomePageState extends State<MyHomePage> {

  // omitted

  
  Widget build(BuildContext context) {
    return Scaffold(
      
      // omitted

      floatingActionButton: FloatingActionButton(
        onPressed: () {
-          AnchorElement(href: 'data:text/plain;charset=utf-8,$_counter')
-            ..setAttribute('download', 'counter.txt')
-            ..click();          
+          download('counter.txt', _counter.toString());
        },
        tooltip: 'Download',
        child: const Icon(Icons.download),
      ),
    );
  }
}

If you want to export, you will have to prepare a separate intermediary file such as util.dart and import it from the Widget side. (I will omit it here.)

util.dart
export './utils/util_io.dart' 
    if (dart.library.html) './utils/util_html.dart';

You can now run your tests on the Native platform, avoiding errors caused by Web-dependent code.

Let's also create stubs for the Native platform for Web platform-dependent external packages

Our system uses Keycloak as its authentication infrastructure. The following package is used for Keycloak authentication on Flutter web applications.

If you open the link, you'll see this package only supports the web.

Thanks to this package, the authentication process was implemented with ease. However, due to the nature of the authentication module, its interface is used in various places. Consequently, all API calls and other widgets that require authentication information are dependent on the web platform, making it impossible to test with CI. (In the meantime, we have been testing locally with the --platform chrome option, and if all passed, it was OK.)

In addition, when you import this package, the following error occurs during test execution.


Error: Dart library 'dart:js_util' is not available on this platform.

Therefore, I will do the same procedure for external packages as the aforementioned import separation, but here I would like to practice the pattern using export. The procedure is as follows.

1. Creating an intermediary package

As an example, I have created a package called inter_lib in the sample code package.

flutter create inter_lib --template=package

In the actual product code, the external package is intermediated by creating a package separate from the product in order to prevent the code according to the external package from being mixed in the product code. I recommend using Melos because it makes multi-package development easy.

2. Creating a stub for the Native platform

To create a stub for keycloak_flutter, refer to the Github repository and simulate the interface (Please check the license as appropriate). All classes and methods used on the product code are required.

The file created appears as follows. A prefix of stub_ below the src directory is a simulation of the external package interface.

inter_lib
├── lib
│   ├── keycloak.dart
│   └── src
│       ├── stub_keycloak.dart
│       ├── stub_keycloak_flutter.dart
│       └── entry_point.dart

Also, entry_point.dart was defined to export the same as the actual external package (In fact, only the interface used in the product code is sufficient).

stub_keycloak_interface.dart

export './stub_keycloak.dart'
    show
        KeycloakConfig,
        KeycloakInitOptions,
        KeycloakLogoutOptions,
        KeycloakLoginOptions,
        KeycloakProfile;
export './stub_keycloak_flutter.dart';

To internally publish this inter_lib as a package, configure export as follows.

keycloak.dart

library inter_lib;

export './src/entry_point.dart'
    if (dart.library.html) 'package:keycloak_flutter/keycloak_flutter.dart';

3. Add he intermediary package to dependencies in pubspec.yaml

Add a relative path to inter_lib to pubspec.yaml.

pubspec.yaml

// omitted

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
+  inter_lib:
+    path: './inter_lib'

// omitted

Then, replace the original reference to an external package with inter_lib.

main.dart
- import 'package:keycloak_flutter/keycloak_flutter.dart';
+ import 'package:inter_lib/keycloak.dart';
import 'package:flutter/material.dart';
import 'package:sample_web/my_home_page.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final keycloakService = KeycloakService(
    KeycloakConfig(
      url: 'XXXXXXXXXXXXXXXXXXXXXX',
      realm: 'XXXXXXXXXXXXXXXXXXXXXX',
      clientId: 'XXXXXXXXXXXXXXXXXXXXXX',
    ),
  );

  await keycloakService.init(
    initOptions: KeycloakInitOptions(
      onLoad: 'login-required',
      enableLogging: true,
      checkLoginIframe: false,
    ),
  );

  runApp(
    const MyApp(),
  );
}

The above outlines the process of creating a stub for the Native platform of a Web platform-dependent external package. Now the test can be run in VM. This method can of course be applied in addition to the keycloak_flutter used in this example.

successful people

Summary

This article summarized our approach to maintaining Flutter Web code testable.

  • Dart's execution environment includes a Web platform and a Native platform
  • flutter test is a native platform execution, and if using a package for a web platform such as dart:html would cause an error
  • It can be solved with an implementation that switches between the real package and stub for each platform, utilizing the dart.library.io and dart/library/html flags
脚注
  1. https://dart.dev/web ↩︎

  2. https://docs.flutter.dev/platform-integration/web/building#add-web-support-to-an-existing-app ↩︎

  3. https://dart.dev/guides/libraries/create-packages#conditionally-importing-and-exporting-library-files ↩︎

Facebook

関連記事 | Related Posts

We are hiring!

【Toyota Woven City決済プラットフォームフロントエンドエンジニア(Web/Mobile)】/Woven Payment Solution開発G/東京

Woven Payment Solution開発グループについて我々のグループはトヨタグループが取り組むToyota Woven Cityプロジェクトの一部として、街の中で利用される決済システムの構築を行います。Toyota Woven Cityは未来の生活を実験するためのテストコースとしての街です。

【Woven City決済プラットフォーム構築 PoC担当バックエンドエンジニア(シニアクラス)】/Woven Payment Solution開発G/東京

Woven Payment Solution開発グループについて私たちのグループはトヨタグループが取り組むWoven Cityプロジェクトの一部として、街の中で利用される決済システムの構築を行います。Woven Cityは未来の生活を実験するためのテストコースとしての街です。