導入
過去に私はモバイルアプリエンジニアとして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固有のテストとも言えます。
これが誰かの役に立てば幸いです。