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:
- A Kotlin Engineer's Introduction to Flutter and Making a Web App Within a Month
- The Best Practices Found by Backend Engineers While Developing Multiple Flutter Applications at Once
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.
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).
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%.
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.
import 'dart:html';
void download(String fileName, String data) {
AnchorElement(href: 'data:text/plain;charset=utf-8,$data')
..setAttribute('download', fileName)
..click();
}
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.
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.)
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).
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.
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
.
// 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
.
- 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.
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 asdart: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
anddart/library/html
flags
関連記事 | Related Posts
A Kotlin Engineer’s Journey Building a Web Application with Flutter in Just One Month
KotlinエンジニアがFlutterに入門して1ヶ月でWebアプリケーションを作った話
The Best Practices Found by Backend Engineers While Developing Multiple Flutter Applications at Once
バックエンドエンジニアたちが複数のFlutterアプリを並行開発していく中で見つけたベストプラクティス
Woven Payment Solution開発G紹介
Key Aspects of QA Testing for Native Apps
We are hiring!
【Toyota Woven City決済プラットフォームフロントエンドエンジニア(Web/Mobile)】/Toyota Woven City Payment Solution開発G/東京
Toyota Woven City Payment Solution開発グループについて我々のグループはトヨタグループが取り組むToyota Woven Cityプロジェクトの一部として、街の中で利用される決済システムの構築を行います。Toyota Woven Cityは未来の生活を実験するためのテストコースとしての街です。
【Woven City決済プラットフォーム構築 PoC担当バックエンドエンジニア(シニアクラス)】/Toyota Woven City Payment Solution開発G/東京
Toyota Woven City Payment Solution開発グループについて私たちのグループはトヨタグループが取り組むWoven Cityプロジェクトの一部として、街の中で利用される決済システムの構築を行います。Woven Cityは未来の生活を実験するためのテストコースとしての街です。