0
0

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でアプリを作っていました。
当時は、「実装する」ことがメインになっていたので、テストコードを書いていませんでした。
(バックエンド側はユニットテストコードを書いていましたが、モバイル側は意識していませんでした。)

今になって色々調べてみると、Flutterには、

  • Unit Test
  • Widget Test
  • Integration Test
    の3種類があるということを知りましたので、
    試しにこれらを実装してみることにしました。

まず今回はWidget Testを実装してみたので、その解説をしていきます。

テスト実施

テスト対象

以下のWidgetをテスト対象としました。
ちなみにソース上のlistProviderは前回のUnit Testの記事と同じものになります。

もし、ソースが気になる方はそちらを参照してください。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hoge/presentation/view_models/list_provider.dart';

class ExpandedListView extends ConsumerWidget {
  const ExpandedListView({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final listState = ref.watch(listProvider);
    return Expanded(
      child: ListView.builder(
        shrinkWrap: true,
        physics: const NeverScrollableScrollPhysics(),
        scrollDirection: Axis.vertical,
        itemCount: listState.dateList.length,
        itemBuilder: (BuildContext context, int index) {
          return Container(
            margin: const EdgeInsets.all(10),
            padding: const EdgeInsets.all(10),
            decoration: BoxDecoration(
              color: Theme.of(context).colorScheme.primary,
              borderRadius: BorderRadius.circular(10),
            ),
            child: Text(
              listState.dateList[index],
              style: Theme.of(context).textTheme.bodyText1,
            ),
          );
        },
      ),
    );
  }
}

listProviderには、dateListの中にString型で日付が格納されています。
それを今回のWidgetでは、dateListの中身をContainerとして出力し、Textで日付を表示するものになっています。

今回のWidgetテストでは、

  • dataListのデータ数がContainer数であること
  • dataListの中身とTextで表示されている文字(日付)が同じであること
    を満たすことを観点としました。

テストコード

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hoge/presentation/view_models/list_provider.dart';
import 'package:hoge/presentation/widgets/expanded_list_view.dart';

void main() {
  const mockState = ListState(dateList: ['2023-01-01', '2023-01-02']);

  // Container Widgetの数の検査
  testWidgets('ExpandedListView displays correct number of items',
      (WidgetTester tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          listProvider.overrideWith((ref) => MockListStateNotifier(mockState)),
        ],
        child: const MaterialApp(
          home: Scaffold(
            body: Column(
              children: [
                ExpandedListView(),
              ],
            ),
          ),
        ),
      ),
    );

    // ListViewが1つであること
    expect(find.byType(ListView), findsOneWidget);
    // ContainerがmockState.dataListと同数であること
    expect(find.byType(Container),findsNWidgets(mockState.dateList.length));
    
  });

  testWidgets('ExpandedListView displays correct text',
      (WidgetTester tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          listProvider.overrideWith((ref) => MockListStateNotifier(mockState)),
        ],
        child: const MaterialApp(
          home: Scaffold(
            body: Column(
              children: [
                ExpandedListView(),
              ],
            ),
          ),
        ),
      ),
    );

    // Textに表示されている文言がmockState.dateListの中身と一致していること
    expect(find.text(mockState.dateList[0]), findsOneWidget);
    expect(find.text(mockState.dateList[1]), findsOneWidget);
  });
}

// 重要: ListStateNotifierのMock用のクラス
class MockListStateNotifier extends ListStateNotifier {
  MockListStateNotifier(ListState initialState) : super() {
    state = initialState;
  }
}

テストは問題なく成功しました。
ただ、1点重要な点が含まれます。
それは、今回のテスト対象にはProviderが含まれていることです。
ProviderのStateは通常デフォルト値が設定されており、今回のListStateNotifierにおいても「空のdateList」がデフォルトとなっていました。
したがって、テストコードに書いてあるmockStateをテスト対象のProviderのdateListに格納することができません。

そこで、テストコードの最後に定義しているMockListStateNotifierというクラスが役立ちます。
これはListStateNotifierを継承しているため、ListStateNotifierとして振る舞うことが可能です。
このクラスをtester.pumpWidgetの中で、利用することで、dataListのmockStateのリストを設定することができています。

  • 利用箇所
ProviderScope(
  overrides: [
      listProvider.overrideWith((ref) => MockListStateNotifier(mockState)),
  ],
  // 省略
)

最後に

今回はWidget Testを書いてみました。
このテストはWebアプリにおけるフロントエンド開発のComponentテストに近いものになります。
ただ、Widget Testという言葉でいうとFlutterの特徴の一つであるWidgetという単位でのテストになり、Flutter固有のテストとも言えます。
これが誰かの役に立てば幸いです。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?