5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Flutter x Riverpod x hive] 簡単なブックマークアプリとWidget Test、Integration Test

Last updated at Posted at 2022-01-30

作成したブックマークアプリ

ポイント

アプリ

URLからページのタイトルやサムネ画像を取得する

過去に記載↓

Riverpod x hive

hiveはBoxアクセス前にHive.openBox(非同期)を実行する必要があるためFutureProviderで宣言。(いろいろ試したがこれが一番しっくりきた)

bookmark_repository.dart
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で変更を検知することとした。

bookmark_list.dart
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から確認。(考えてみればどうせ表示できないので適当でもいいかもしれない。。。)

test/home_test.dart
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)で上書きする。

test/home_test.dart
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でもできるかもしれない)

integration_test/home_test.dart
    // ブックマークが表示されるか確認
    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}"
5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?