Riverpod 2.4 のテストにおけるモック
Riverpod 1.0.3 から Riverpod 2.4にマイグレーションした時に test の書き味が少し変わっていることで戸惑ってしまったため書き残しておく
- Riverpod 2.0 で一部 Provider の overrideWithValue が撤去されて、代わりにoverrideWithProvider を利用することになった
- Riverpod 2.1 では overrideWithProvider が deprecated になり、代わりに追加されたのが
provider.overrideWith((ref) => state)
- overrideWithValue が撤去されたのは一時的である(This change is temporary)とされている...
- 初期の頃のように state の mock が一律 overrideWithValue でできればテストが書きやすいのになあと思う今日この頃
- 本家のテスト系のドキュメントはこの辺とかこの辺とか
動作環境
- riverpod: 2.4.0
- mocktail: 1.0.0
- golden_toolkit: 0.15.0
準備
本家に習って test のディレクトリに util を追加しておこう。 unit_test で ProviderContainer をテスト後に後始末するお作法を毎回記入するのは面倒くさい:
import 'package:flutter_test/flutter_test.dart';
import 'package:riverpod/riverpod.dart';
ProviderContainer createContainer({
List<Override> overrides = const [],
}) {
final container = ProviderContainer(
overrides: overrides,
);
addTearDown(container.dispose); // これ
return container;
}
Provider
final countProvider = Provider<int>((ref) => 0);
Provider の test には 従来どおり overrideWithValue が提供されてる:
test('count provider ...', () {
final container = createContainer(
overrides: [
countProvider.overrideWithValue(153),
],
);
expect(container.read(countProvider), 153);
});
overrideWith を使った書き方もできる:
test('count provider ...', () {
final container = createContainer(
overrides: [
countProvider.overrideWith((ProviderRef<int> ref) => 153),
// 省略可
// countProvider.overrideWith((ref) => 153),
],
);
expect(container.read(countProvider), 153);
});
StateProvider
StateProvider も同様、overrideWith でモックするだけ:
final countProvider = StateProvider<int>((ref) => 0);
test('count provider ...', () {
final container = createContainer(
overrides: [
countProvider.overrideWith((StateProviderRef<int> ref) => 153),
// 省略可
// countProvider.overrideWith((ref) => 153),
],
);
expect(container.read(countProvider), 153);
});
FutureProvider
FutureProvider も同様、overrideWith でモックするだけ。
RiverPod 2.0 から AsyncValue にテストに使える便利なプロパティがいくつか追加されている:
final countProvider = FutureProvider<int>((ref) async => 0);
test('count provider ...', () async {
final container = createContainer(
overrides: [
countProvider.overrideWith((FutureProviderRef<int> ref) async => 153),
// 省略可
// countProvider.overrideWith((ref) async => 153),
],
);
expect(container.read(countProvider), const AsyncLoading<int>());
expect(container.read(countProvider).isLoading, true);
await container.read(countProvider.future);
expect(container.read(countProvider), const AsyncData(153));
expect(container.read(countProvider).value, 153);
expect(container.read(countProvider).isLoading, false);
});
StateNotifierProvider
StateNotifierProvider も同様、overrideWith でモックするだけ:
final countProvider =
StateNotifierProvider<CountNotifier, int>((ref) => CountNotifier());
class CountNotifier extends StateNotifier<int> {
CountNotifier([super.initialValue = 0]);
}
test('count provider ...', () {
final container = createContainer(
overrides: [
countProvider.overrideWith(
(StateNotifierProviderRef<CountNotifier, int> ref) =>
CountNotifier(153),
// 省略可
// countProvider.overrideWith((ref) => CountNotifier(153)),
),
],
);
expect(container.read(countProvider), 153);
});
StateNotifierProvider + mocktail
mocktail で State をモックする場合はひと手間必要:
test('count provider ...', () {
final mockCountNotifier = MockCountNotifier(153);
final container = createContainer(
overrides: [
countProvider.overrideWith((ref) => mockCountNotifier),
],
);
expect(container.read(countProvider), 153);
});
class MockCountNotifier extends StateNotifier<int>
with Mock
implements CountNotifier {
MockCountNotifier(int initialValue) : super(initialValue);
}
StateNotifierProvider.autoDispose + mocktail + golden_toolkit
WidgetTest でもやることは変わらない、 golden_toolkit で Golden Test を試してみよう:
簡単なカウントアプリを作る:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope(child: MyApp()),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(home: Home());
}
}
class Home extends ConsumerWidget {
const Home({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: const Text('Counter example')),
body: Center(
child: Text('${ref.watch(countProvider)}'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(countProvider.notifier).increment(),
child: const Icon(Icons.add),
),
);
}
}
final countProvider = StateNotifierProvider.autoDispose<CountNotifier, int>(
(ref) => CountNotifier());
class CountNotifier extends StateNotifier<int> {
CountNotifier([super.initialValue = 0]);
void increment() => state++;
}
Golden Test でカウンタの数値をモックする
golden_toolkit の 複数デバイスのテストでは、シナリオごとに AutoDispose が動作してしまうためモックする位置に注意すること。さもなくば、Bad state: Tried to use * after dispose was called.
でテストがコケてしまう。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
import 'package:mocktail/mocktail.dart';
import 'package:riverpod_test_demo/main.dart';
void main() {
testGoldens('DeviceBuilder - one scenario - default devices', (tester) async {
final builder = DeviceBuilder()
..addScenario(
widget: Builder(builder: (context) {
final countNotifier = MockCountNotifier(153);
return ProviderScope(
overrides: [
countProvider.overrideWith((ref) => countNotifier),
],
child: const MyApp(),
);
}),
name: 'default page',
);
await tester.pumpDeviceBuilder(builder);
await screenMatchesGolden(tester, 'flutter_demo_page_single_scenario');
// Pending timers エラーの workaround:
// https://github.com/rrousselGit/riverpod/issues/1941#issuecomment-1325256770
await tester.pumpWidget(Container());
await tester.pumpAndSettle();
});
}
class MockCountNotifier extends StateNotifier<int>
with Mock
implements CountNotifier {
MockCountNotifier(int initialValue) : super(initialValue);
}
Golden 出力
Golden Test について一応補足
VSCode 構成などは、Getting Started を読もう
特に、Golden Test の出力のテキスト部分が Ahem というテストフォントで黒く塗りつぶされないようにするには一手間必要:
/// test/flutter_test_config.dart
import 'dart:async';
import 'dart:io';
import 'package:golden_toolkit/golden_toolkit.dart';
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
return GoldenToolkit.runWithConfiguration(
() async {
await loadAppFonts();
await testMain();
},
config: GoldenToolkitConfiguration(
skipGoldenAssertion: () => !Platform.isMacOS,
),
);
}
参考
Discussion