はじめに
テストを行う際に特定のパッケージをmockすることは多いです。
代表的な手法はmockitoを使って対象のパッケージ、クラスをmockすることです。
しかし、今回表題にあるflutter_app_badgerをmockする際に少々手こずったので記事に残したいと思います。
記事の対象者
- flutter_app_badgerのmock化にお困りの方
- Method Chanelのmock方法を知りたい方
記事を執筆時点での筆者の環境
[✓] Flutter (Channel stable, 3.22.1, on macOS 14.3.1 23D60 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 15.2)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2023.3)
[✓] VS Code (version 1.90.1)
flutter_app_badgerとは?
デバイスのホーム画面に表示されるアプリアイコンの通知バッジに対して、簡易的な操作を行うパッケージです。
細かいインストール方法や使い方の説明は割愛します。
今回はあくまでテストに関してお話ししますので、パッケージの使用方法などについて詳しくは以下の記事をご覧ください。
1. 実装内容
一旦テストコードを見る前に実装内容を簡単にご紹介します。
1-1. ディレクトリ構成
今回はレイヤードアーキテクチャにそって実装しています。
riverpodでDIを行っています。
riverpodは自動生成のコードを使用しています。
本来はパッケージのインスタンスを分けて実装しますが、flutter_app_badgerは静的メソッドのみでインスタンスを持っていません。
よって今回はlocal_sourcesディレクトリを使わずリポジトリ層で直接メソッドを呼び出しています。
この「静的メソッド」という部分が曲者です。
├── local_sources
│ └── // <===== 💡 本来であればここにapp_badger.dartでインスタンスを管理するが今回はなし
└── repositories
└── app_badger_repository
├── provider.dart
├── provider.g.dart
└── repository.dart
1-2. repositoryとprovider
@Riverpod(keepAlive: true)
AppBadgerRepositoryBase appBadgerRepository(AppBadgerRepositoryRef ref) {
return AppBadgerRepository(ref);
}
abstract interface class AppBadgerRepositoryBase {
Future<void> updateBadge({required int count});
}
class AppBadgerRepository implements AppBadgerRepositoryBase {
AppBadgerRepository(this.ref);
final ProviderRef<dynamic> ref;
@override
Future<void> updateBadge({required int count}) async {
// FlutterAppBadgerがサポートできるかどうか確認
final isSupported = await FlutterAppBadger.isAppBadgeSupported();
logger.d('isAppBadgeSupported: $isSupported');
if (isSupported) {
await FlutterAppBadger.updateBadgeCount(count);
} else {
logger.d('サポート対象外のデバイスです');
}
}
}
1-3. 使用例
class LifeCycle extends HookConsumerWidget {
const LifeCycle({required this.child, super.key});
final Widget child;
@override
Widget build(BuildContext context, WidgetRef ref) {
useOnAppLifecycleStateChange((_, AppLifecycleState currentState) {
// アプリがバックグラウンドからフォアグラウンドに移動した時の処理
if (currentState == AppLifecycleState.resumed) {
// 通知のバッジをリセット
ref.read(appBadgerRepositoryProvider).updateBadge(count: 0);
}
});
return child;
}
}
2. テストコードの解説
先ほども述べましたが、flutter_app_badgerの機能、メソッドは全て静的メソッドです。
つまり、インスタンスがないのでmockitoが使えません。
他のパッケージでも静的メソッドで定義しているケースはあるものの、テスト用のインスタンスを取得する方法が用意されていたりします。
しかしこのパッケージに関しては完全に手段がありません。
そこで、大元のMethodChanel
をmockする方法をとります。
2-1. テストコード全体
void main() {
late ProviderContainer container;
const channel = MethodChannel('g123k/flutter_app_badger');
final log = <MethodCall>[];
const upDateBadgeCount = 'updateBadgeCount';
const removeBadge = 'removeBadge';
const isAppBadgeSupported = 'isAppBadgeSupported';
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
container = ProviderContainer();
// メソッドチャネルのレスポンスを設定
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
switch (methodCall.method) {
case upDateBadgeCount:
final args = methodCall.arguments as Map<dynamic, dynamic>;
final count = args['count'] as int;
expect(count, 0);
log.add(methodCall);
return null;
case removeBadge:
log.add(methodCall);
return null;
case isAppBadgeSupported:
log.add(methodCall);
return true;
default:
return null;
}
});
});
tearDown(() {
container.dispose();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null);
log.clear();
});
test('サポートされているデバイスであれば通知のバッジをアップデートできる', () async {
await container.read(appBadgerRepositoryProvider).updateBadge(count: 0);
expect(log, hasLength(2));
expect(log[0].method, isAppBadgeSupported);
expect(log[1].method, upDateBadgeCount);
expect(log[1].arguments, {'count': 0});
});
}
2-2. 部分解説
内部実装の確認
まずはflutter_app_badgerの内部実装を見て必要な情報を控えます。
必要なのはMethodChannel
に設定されている文字列と、各メソッドのキーになっている文字列です。
import 'dart:async';
import 'package:flutter/services.dart';
class FlutterAppBadger {
static const MethodChannel _channel =
// 👇 これ!
const MethodChannel('g123k/flutter_app_badger');
static Future<void> updateBadgeCount(int count) {
// 👇 これ!
return _channel.invokeMethod('updateBadgeCount', {"count": count});
}
static Future<void> removeBadge() {
// 👇 これ!
return _channel.invokeMethod('removeBadge');
}
static Future<bool> isAppBadgeSupported() async {
// 👇 これ!
bool? appBadgeSupported = await _channel.invokeMethod('isAppBadgeSupported');
return appBadgeSupported ?? false;
}
}
定数を準備
MethodChannel
とそれぞれのメソッドを呼び出すキーとなる文字列を定数化しておきます。
更にメソッドが呼ばれたら値を格納する配列をfinal log = <MethodCall>[];
で定義します。
const channel = MethodChannel('g123k/flutter_app_badger');
final log = <MethodCall>[];
const upDateBadgeCount = 'updateBadgeCount';
const removeBadge = 'removeBadge';
const isAppBadgeSupported = 'isAppBadgeSupported';
setUp
内でMethodChanelのスタブを作成します。これはmockitoでいうところのwhen()
です。
TestDefaultBinaryMessengerBinding
を使ってレスポンスを設定していきます。
各ケース内でそれぞれのメソッドが実行された時にlog.add(methodCall);
で配列内に格納していっています。
setUp(() {
container = ProviderContainer();
// メソッドチャネルのレスポンスを設定
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
switch (methodCall.method) {
case upDateBadgeCount:
final args = methodCall.arguments as Map<dynamic, dynamic>;
final count = args['count'] as int;
expect(count, 0);
log.add(methodCall);
return null;
case removeBadge:
log.add(methodCall);
return null;
case isAppBadgeSupported:
log.add(methodCall);
return true;
default:
return null;
}
});
});
あとはメソッドが呼ばれたかどうかをlog
を使って検証すれば完了です。
test('サポートされているデバイスであれば通知のバッジをアップデートできる', () async {
await container.read(appBadgerRepositoryProvider).updateBadge(count: 0);
expect(log, hasLength(2));
expect(log[0].method, isAppBadgeSupported);
expect(log[1].method, upDateBadgeCount);
expect(log[1].arguments, {'count': 0});
});
終わりに
いかがだったでしょうか?
正直、このようにMethodChanelをmockすることはかなりのレアケースだと思います。
MethodChanelをmockできることを知っていればいざという時に対応できるので、
頭の片隅にでも留めておきましょう。
この記事が誰かのお役に立てれば幸いです。
参考記事