Tests in a flutter project or game often require assets that you don’t want in the root asset bundle (rootBundle) -w specially if you are writing golden tests. Wouldn’t it be great if you could create a custom asset bundle for your tests?
Today I ran in to this exact problem. I’ve written an image utility class that resizes images, and supports other transforms at the same time (like scale, rotation and crop). I need some image assets to test this class that I don’t want as part of the rootBundle. Flutter doesn’t currently support “dev” assets – see open issue #5813 – but luckily it is possible to create your own custom asset bundle.
I based my asset bundle class on this one that a Google Flutter engineer posted in a thread on another issue. I modified it to work better for my purpose.
Asset bundle class
This is the code for the custom asset bundle:
import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:glob/glob.dart';
import 'package:glob/list_local_fs.dart';
/// A custom [AssetBundle] that reads files from a directory.
///
/// This is meant to be used in place of [rootBundle] for testing
class DiskAssetBundle extends CachingAssetBundle {
static const _assetManifestDotJson = 'AssetManifest.json';
/// Creates a [DiskAssetBundle] by loading files from [path].
static Future<AssetBundle> loadFromPath(
String path, {
String? from,
}) async {
// Prepare the file search pattern
path = _formatPath(path);
String pattern = path;
if (!pattern.endsWith('/')) {
pattern += '/';
}
pattern += '**';
// Load the assets
final cache = <String, ByteData>{};
await for (final entity in Glob(pattern).list(root: from)) {
if (entity is File) {
final bytes = await (entity as File).readAsBytes();
// Keep only the asset name relative to the folder
String name = _formatPath(entity.path);
name = name.substring(name.indexOf(path) + path.length);
cache[name] = ByteData.view(bytes.buffer);
}
}
// Create the asset manifest
final manifest = <String, List<String>>{};
cache.forEach((key, _) {
manifest[key] = [key];
});
cache[_assetManifestDotJson] = ByteData.view(
Uint8List.fromList(jsonEncode(manifest).codeUnits).buffer,
);
return DiskAssetBundle._(cache);
}
/// Format a file path to only forward slashes
static String _formatPath(String path) {
return path.replaceAll(r'\', '/');
}
/// The cache of assets
final Map<String, ByteData> _cache;
/// Private constructor
DiskAssetBundle._(this._cache);
/// Load an asset from the cache
@override
Future<ByteData> load(String key) async {
return _cache[key]!;
}
}
DartWriting a golden test
This is how you use it for a golden test:
import 'dart:io';
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'disk_asset_bundle.dart';
void main() async {
// Path to the goldens folder (i.e. project/test/_goldens)
String goldens =
'${Directory.current.path.replaceAll(r'\', '/')}/test/_goldens';
// This is required before an asset bundle is available
TestWidgetsFlutterBinding.ensureInitialized();
// Create the custom assets bundle from the resources folder
// (i.e. project/test/_resources)
AssetBundle assets = await DiskAssetBundle.loadFromPath('test/_resources/');
group('Example', () {
/// This test will compare an image to a golden file
test('Does match golden', () async {
final image = await assets.load('image.png');
await expectLater(
image,
matchesGoldenFile('$goldens/image-no-transform.png'),
);
});
});
}
DartSome tips, especially if you are new to Flutter or dart testing:
- A golden test is a comparison of a generated image against a golden image to ensure that the generated image is correct. Every pixel of the image has to exactly match the same pixel in the golden image.
- After you have tested your code yourself and it is working as expected, you generate goldens. These are included in the repo. The golden tests then ensure that future changes do not affect how the images are generated.
- To generate the golden images (just once):
flutter test --update-goldens
- To run the tests once you have generated goldens:
flutter test
- My file structure is like this:
project root/
test/
_goldens/
_resources/
image.png
example_test.dart
disk_asset_bundle.dart
Hopefully you can figure it out from here!