Flutter カスタム Hook をモックし、 Widget のテスト向けに置き換え
はじめに
Flutter Hooksは React Hooks のようにuseState
やuseEffect
が Flutter で使用できます。
また、Flutter Hooks で用意されたものだけでなく、カスタム Hook を作ることも可能です。
カスタム Hook を使用することで Widget とロジックを分離でき、見通しのよいコード・Widget になると思いました。
ただカスタム Hook は関数型のため、mockito でモックを自動生成できません。
そのため、Widget 向けのテストでカスタム Hook のモックを用意し、Widget のコンストラクタの引数でカスタム Hook のモックを指定・依存性の注入(DI)すれば、置き換えられると考えました。
環境
- flutter: 3.16.4
- flutter_hooks: 0.20.4
カスタム Hook の作成
Widget 向けのテストを書く前に、まずはカスタム Hook を用意します。
flutter create
で最初に作成されるカウントアップを例にします。
lib/use_counter.dart
import 'package:flutter_hooks/flutter_hooks.dart';
typedef UseCounterReturn = ({
int state,
void Function() increment,
});
typedef UseCounter = UseCounterReturn Function();
UseCounterReturn useCounter() {
final state = useState(0);
final increment = useCallback(() {
state.value++;
}, [state]);
return (
state: state.value,
increment: increment,
);
}
lib/my_home_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'use_counter.dart';
class MyHomePage extends HookWidget {
const MyHomePage({
super.key,
required this.title,
});
final String title;
Widget build(BuildContext context) {
final counter = useCounter();
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
key: const Key('counter'),
counter.state.toString(),
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
key: const Key('incrementButton'),
onPressed: counter.increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
Widget のテストでカスタム Hook をモックし、置き換え
コンストラクタの引数を追加
カスタム Hook を置き換え・依存性の注入(DI)できるように、コンストラクタに引数を追加します。
引数が指定されてない場合は、デフォルトとしてモックではないカスタム Hook を指定し、テスト以外では指定しないようにしています。
lib/my_home_page.dart 抜粋
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'use_counter.dart' as hook;
class MyHomePage extends HookWidget {
const MyHomePage({
super.key,
required this.title,
this.useCounter = hook.useCounter,
});
final String title;
final hook.UseCounter useCounter;
Widget build(BuildContext context) {
final counter = useCounter();
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
-import 'use_counter.dart';
+import 'use_counter.dart' as hook;
class MyHomePage extends HookWidget {
const MyHomePage({
super.key,
required this.title,
+ this.useCounter = hook.useCounter,
});
final String title;
+ final hook.UseCounter useCounter;
@override
Widget build(BuildContext context) {
Widget のテスト
test/my_home_page_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../lib/my_home_page.dart';
import '../lib/use_counter.dart';
void main() {
group('MyHomePage', () {
testWidgets('counterが表示されること', (WidgetTester tester) async {
UseCounterReturn useCounterMock() {
return (
state: 9,
increment: () {},
);
}
await tester.pumpWidget(MaterialApp(
home: MyHomePage(title: 'title', useCounter: useCounterMock)));
await tester.pumpAndSettle();
final counterFinder = find.byKey(const Key('counter'));
expect(counterFinder, findsOneWidget);
final counterText = counterFinder.evaluate().first.widget as Text;
expect(counterText.data, '9');
});
testWidgets('FABをタップして、incrementが呼ばれること', (WidgetTester tester) async {
var incrementCalled = false;
UseCounterReturn useCounterMock() {
return (
state: 0,
increment: () {
incrementCalled = true;
},
);
}
await tester.pumpWidget(MaterialApp(
home: MyHomePage(title: 'title', useCounter: useCounterMock)));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('incrementButton')));
expect(incrementCalled, true);
});
});
}
まとめ
Widget 向けのテストでカスタム Hook をモックを用意し、置き換えることができました。
カスタム Hook を使用し Widget とロジックを分離していて、Widget 向けのテストでは分離する方法を考えている方は、Widget のコンストラクタの引数でカスタム Hook のモックを指定する方法はいかがでしょうか。
最後まで読んでいただきありがとうございます。この記事がすこしでもよいと思ったら、Like♥ を押してもらえると励みになります。
Discussion