作成したブックマークアプリ
Flutter x Riverpod x HiveのBookmarkアプリ pic.twitter.com/diaCF4wlig
— かーにゃ (@popy1017) January 30, 2022
ポイント
アプリ
URLからページのタイトルやサムネ画像を取得する
過去に記載↓
Riverpod x hive
hiveはBoxアクセス前にHive.openBox
(非同期)を実行する必要があるためFutureProvider
で宣言。(いろいろ試したがこれが一番しっくりきた)
final bookmarkRepositoryProvider =
FutureProvider((ref) => BookmarkRepository.open());
class BookmarkRepository {
late Box<Bookmark> _box;
Box<Bookmark> get box => _box;
List<Bookmark> get bookmarks => _box.values.toList();
Stream<BoxEvent> get stream => _box.watch();
static Future<BookmarkRepository> open() async {
final repos = BookmarkRepository();
await repos._initHive();
return repos;
}
Future<BookmarkRepository> _initHive() async {
Hive.registerAdapter(BookmarkAdapter());
_box = await Hive.openBox('bookmark');
return this;
}
~~
}
View部分
hiveにはDBの変更を検知するためのメソッド(box.listenable()
)が用意されており、それをValueListenableBuilder
と組み合わせて簡単に変更を検知することができるが、ValueListenableBuilder
のテスト方法がわからなかったのでbox.watch()
とStreamBuilder
で変更を検知することとした。
class BookmarkList extends ConsumerWidget {
const BookmarkList({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final _asyncBookmarkRepository = ref.watch(bookmarkRepositoryProvider);
return _asyncBookmarkRepository.when(
data: (BookmarkRepository _bookmarkRepository) {
// ValueListenableBuilderとbox.listenable()でも変更を検知できるが
// テストしづらいためStreamBuilderとbox.watch()を採用
return StreamBuilder(
stream: _bookmarkRepository.stream,
builder: (_, AsyncSnapshot<BoxEvent> snapshot) {
~~
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => const Center(child: CircularProgressIndicator()),
);
}
}
参考
テスト
Widget test
Repositoryのモック化
mockitoを使いBookmarkRepository
をモック化。テストに必要な関数をoverride
。
サムネ画像のURLはOGPを確認できるサイトや、サイトのhtmlから確認。(考えてみればどうせ表示できないので適当でもいいかもしれない。。。)
class MockBookmarkRepository extends Mock implements BookmarkRepository {
final List<Bookmark> _bookmarks = [
Bookmark(
id: '123',
url: 'https://flutter.dev/',
title: 'Flutter',
description: 'Flutter web site',
imageUri:
'https://storage.googleapis.com/cms-storage-bucket/70760bf1e88b184bb1bc.png',
),
Bookmark(
id: '456',
url: 'https://www.famitsu.com/news/202201/28249315.html',
title: 'Famitsu',
description: 'Famitsu web site',
imageUri:
'https://www.famitsu.com/images/000/249/315/z_61f265395b2da.jpg',
)
];
final StreamController<BoxEvent> _streamController =
StreamController<BoxEvent>();
@override
List<Bookmark> get bookmarks => _bookmarks;
@override
Stream<BoxEvent> get stream => _streamController.stream;
@override
Future<void> create(Bookmark bookmark) async {
_bookmarks.add(bookmark);
// Box変更イベントを発火(BoxEventの中身は使わないのでなんでもよい)
_streamController.add(BoxEvent(bookmark.id, '', true));
}
}
モック化したRepositoryクラスを使ってテストする
モック化したRepositoryクラスで新たにProviderを作成し、overrideWithProvider(モックProvider)
で上書きする。
void main() {
final BookmarkRepository _fakeRepository = MockBookmarkRepository();
late FutureProvider<BookmarkRepository> _fakeRepositoryProvider;
setUp(() async {
await initialiseHive();
// モック化したRepositoryクラスで新たにProviderを作成。
_fakeRepositoryProvider = FutureProvider((ref) async {
return _fakeRepository;
});
});
testWidgets('登録されているBookmarkが表示される', (WidgetTester tester) async {
// NetworkImageがエラーになるのでモック化
mockNetworkImagesFor(() async {
await tester.pumpWidget(MaterialApp(
home: ProviderScope(
child: Home(),
overrides: [
// `overrideWithProvider(モックProvider)`で上書きする
bookmarkRepositoryProvider.overrideWithProvider(_fakeRepositoryProvider),
],
),
));
~~
Image.network
をモック化する
Widgetテストではhttp通信が入る部分はエラーになるので、network_image_mock
パッケージを使ってモック化する必要がある。
当然URLからタイトルやサムネイル画像を取得する部分もエラーになるので注意が必要(今回はその部分はテストしない)。
~~
testWidgets('登録されているBookmarkが表示される', (WidgetTester tester) async {
// Image.networkがエラーになるのでモック化
mockNetworkImagesFor(() async {
await tester.pumpWidget(MaterialApp(
home: ProviderScope(
child: Home(),
~~
Integration Test
基本的には公式のやり方に沿う。(シミュレーターで動きが見れるのでWidget Testより簡単かもしれない)
自動でスワイプする
今回作成したアプリでは左にスワイプでブックマークを削除する機能があるが、tester.fling
という関数で実現できた。(drag
だとtap
判定になってしまいWebViewが開いてしまったが、頑張ればdrag
でもできるかもしれない)
// ブックマークが表示されるか確認
final bookmark = find.byType(BookmarkCard);
expect(bookmark, findsOneWidget);
// ブックマークを左にスワイプ
await tester.fling(bookmark, Offset(-1000, 0), 5000);
await tester.pumpAndSettle();
// ブックマークが消えているか確認
expect(bookmark, findsNothing);
Hive関連のエラー
Integration testが複数ある場合、Hive.registerAdapter()
が複数回呼ばれてエラーとなってしまうがHive.registerAdapter(HogeAdapter, override: true)
とすれば上書きする設定にできるので対処可能。
Future<void> main({bool isTesting = false}) async {
WidgetsFlutterBinding.ensureInitialized();
// テスト中は複数回実行されてしまうのでoverrideを許可する
Hive.registerAdapter(BookmarkAdapter(), override: isTesting);
Github Actionsで自動化
端末IDを取得する
端末IDを取得しシミュレーターを起動する部分だが、xcrun instruments
が非推奨になっているらしくxcrun xctrace list devices
が代わりに使える。ゴリ押しだが以下のように変更した(awkとかsedとか難しい。。)。
UDID=$(
xcrun xctrace list devices | grep -m 1 "iPhone 13" | sed -e "s/^.*(\(.*\)).*$/\1/"
)
xcrun simctl boot "${UDID:?No Simulator with this name found}"