Flutterテスト
CI/CD
Continuous Integration(継続的インテグレーション)
Continuous Deployment(継続的デプロイ)
CI
開発者が自分のコードを頻繁にリポジトリに統合することを奨励するプラクティスです。各統合は自動化されたビルドとテストのプロセスを通じて検証されます。これにより、コードの品質が保たれ、バグが早期に発見されるようになります。
ステップ
- コードのコミット: 開発者がコードを小さい単位で頻繁にコミットしリポジトリに統合します。
- 自動ビルド: コードがリポジトリにプッシュされると、自動ビルドプロセスがトリガーされます。
- 自動テスト: ビルドされたコードは自動化されたテストスイート(一括テスト)を通じて検証されます。
CD
コードがビルドおよびテストプロセスを通過した後、自動的にデプロイされるプロセスです。これにより、ソフトウェアの新しいバージョンが迅速かつ信頼性高くリリースされることが保証されます。
ステップ
- デプロイ準備: CIプロセスが完了した後、コードがデプロイの準備が整います。
- 自動デプロイ: 自動デプロイプロセスがトリガーされ、ステージング環境または本番環境にリリースされます。
主なサービス
- Github Actions: GitHubリポジトリと統合されたCI/CDツール。ワークフローをYAMLファイルで定義。
- Jenkins: オープンソースの自動化サーバー。プラグインが豊富で柔軟なカスタマイズが可能。
- CircleCI: クラウドベースのCI/CDプラットフォーム。高速なビルドとデプロイメントをサポート。
- GitLab CI/CD: GitLabに組み込まれたCI/CD機能。リポジトリ管理と連携してシームレスなデプロイメントを実現。
GitHubをすでに使っているならGitHub Actionsが総合がスムーズだしセットアップも簡単(逆に言うとカスタマイズ性は低い。それは初心者には問題にはならない)。無料でも使用可能。そしてなにより利用者が多く情報も多い。
GitHub Actions - CI 継続的インテグレーション
数日前からテスト始めたタイミングでCI/CDとはなんぞやと調べていて、テストの後でいい?とAIに訊いたら今すぐやった方がいいよ!と言われたので初めてのGitHub Actions。
なるほど、たしかに早く始めるに越したことはない機能だ。
基本の書き方
まずは基本の基本の形。
CIで何を自動化するのかが良く分かる。
Actionsの公式で自動的に作ってくれるDart用のファイルをFlutter用に書き換えただけ。
GitHub Actions上でUbuntuを使い
- 自分のコードをクローン
- Flutterをインストール
- 各種パッケージをインストール
- 文法や型などコードに問題がないかチェック(静的解析)
- 自分で作ったテストを実行
name: Flutter Build and Test
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # リポジトリのコードをクローン
# Flutter SDKのインストール
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0' # latestはダメだった
- name: Install dependencies
run: flutter pub get
- name: Analyze project source
run: flutter analyze
- name: Run tests
run: flutter test
ちょっと改良
GitHubにプッシュするたびに実行されるので
- Flutter SDKと依存関係のインストールにキャッシュを使う
- パッケージのアップデートの確認
- コードの自動フォーマット
- テスト結果を視覚化してくれるアプリCodecovの導入
name: Flutter Build and Test
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # リポジトリのコードをクローン
- name: Cache Flutter SDK
uses: actions/cache@v3
with:
path: /opt/hostedtoolcache/flutter
key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.yaml') }}
restore-keys: |
${{ runner.os }}-flutter-
# Flutter SDKのインストール
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0' # latestはダメだった
- name: Cache dependencies
uses: actions/cache@v3
with:
path:
~/.pub-cache
~/.flutter
key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.yaml') }}
restore-keys: |
${{ runner.os }}-flutter-
- name: Install dependencies
run: flutter pub get
- name: Check for outdated dependencies
run: flutter pub outdated
- name: Check Code Formatting
run: dart format --set-exit-if-changed .
- name: Analyze project source
run: flutter analyze
- name: Run tests
run: flutter test --coverage # --machine でJSONに形式に変換できる。より高度な自動解析ではJSONにする必要がある。
- name: Upload Code Coverage to Codecov
uses: codecov/codecov-action@v5
with:
file: ./coverage/lcov.info
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
Flutterのテスト
dev_dependencies:
flutter_test:
sdk: flutter
ユニットテスト
ユニットテストは、アプリケーションの中で最小単位のコード(メソッドや関数)を個別にテストする方法です。これにより、各コンポーネントが期待通りに動作することを確認できます。
class Calculator {
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
}
テストコード
import 'package:flutter_test/flutter_test.dart';
import 'package:your_app/calculator.dart';
void main() {
group('Calculator', () {
test('足し算のテスト', () {
final calculator = Calculator();
expect(calculator.add(2, 3), 5); // 2 + 3 = 5を期待
});
test('引き算のテスト', () {
final calculator = Calculator();
expect(calculator.subtract(5, 2), 3); // 5 - 2 = 3を期待
});
});
}
重要なテスト手法
- SetUpとTearDown:
void main() {
setUp(() {
// 各テストケース前に実行するコード
});
tearDown(() {
// 各テストケース後に実行するコード
});
test('テストケースの例', () {
// テストケース
});
}
- Mockの使用
- モックは、外部依存関係をシミュレートするために使用されます。これにより、外部リソース(例: データベース、APIなど)への依存を最小限に抑え、テストの信頼性を向上させます。
ウィジェットテスト
ウィジェットテストは、FlutterアプリケーションのUIコンポーネント(ウィジェット)が期待通りにレンダリングされ、動作するかを検証するテストです。これにより、ユーザーインターフェースの正確性と一貫性を確認することができます。
import 'package:flutter/material.dart';
class MyWidget extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('My Widget'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Hello, World!'),
ElevatedButton(
onPressed: () {},
child: Text('Press me'),
),
],
),
),
),
);
}
}
テストコード
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/my_widget.dart';
void main() {
testWidgets('MyWidget has a title and message', (WidgetTester tester) async {
// MyWidgetをウィジェットツリーに追加
await tester.pumpWidget(MyWidget());
// タイトルを確認
expect(find.text('My Widget'), findsOneWidget);
// メッセージを確認
expect(find.text('Hello, World!'), findsOneWidget);
// ボタンを確認
expect(find.text('Press me'), findsOneWidget);
// ボタンをタップして確認
await tester.tap(find.text('Press me'));
await tester.pump();
// ボタンが押された後の挙動を確認(特に何も起きない場合、この部分は省略)
});
}
重要なテスト手法
- pumpWidgetメソッド:
- ウィジェットツリーにウィジェットを追加してレンダリングします。これにより、テスト対象のウィジェットが表示されます。
- findメソッド:
- ウィジェットツリー内で特定のウィジェットを検索します。例えば、テキストやボタンを検索できます。
- ユーザー操作のシミュレーション:
- tapやenterTextなどのメソッドを使って、ユーザー操作をシミュレーションします。
- pumpメソッド:
- フレームを進めるために使用します。例えば、アニメーションやビルドメソッドの呼び出しをトリガーします。
統合テスト
統合テストは、アプリケーション全体のフローが正しく動作することを確認するテストです。これにより、アプリ全体が一貫して動作することを保証します。
あとで
ユニットテストで使う主な手法
expect
expect(実際の値, 期待する値);
定義
void expect(
dynamic actual,
dynamic matcher,(以下略)
matcher
期待する値と実際の値を比較するためのオブジェクト。
// actualとexpectedが同じか
expect(actual, equals(expected));
// nullチェック
expect(actual, isNull);
expect(actual, isNotNull);
// boolチェック
expect(actual, isTrue);
expect(actual, isFalse);
// 空かどうか
expect(actual, isEmpty);
expect(actual, isNotEmpty);
// 文字列含有
expect('Dart is fun', contains('fun'));
// 型チェック
expect(someObject, isA<MyClass>());
// 例外の時の書き方。someFunctionがSpecificExceptionをスローするか
expect(() => someFunction(), throwsA(isA<SpecificException>()));
// 数値やコレクション
expect(actual, greaterThan(expected));
expect(actual, lessThan(expected));
カスタムmatcher
matcherを作るうえで重要なポイントは
- Matcherクラスを継承
- matchesメソッドのオーバーライド: メイン機能!
- 比較するメソッド。true / false を返す。
- describeメソッドをオーバライド:
- エラーメッセージやデバッグ情報
class HasLength extends Matcher {
final int length;
HasLength(this.length);
// ここがメイン機能!
bool matches(item, Map matchState) {
return item.length == length;
}
// Expectedのログ
Description describe(Description description) {
return description.add('has length $length');
}
// 失敗結果のログ
Description describeMismatch(
item, Description mismatchDescription, Map matchState, bool verbose) {
if (item is! List) {
return mismatchDescription.add('is not a List');
}
return mismatchDescription.add('has length ${item.length}');
}
}
使用例
void main() {
test('custom matcher example', () {
final list = [1, 2, 3];
expect(list, HasLength(3)); // テスト成功(ログ排出されない)
expect(list, HasLength(2)); // テストは失敗。"has length 3"というログが表示される
});
}
mock
mockとは「見せかけの,偽の,うわべだけの」の意味。
テストにおいて、外部のシステムやコンポーネント(データベースやウェブサービスなと)を必要とする際、偽物のオブジェクトを作ることで外部に依存することなくテストを実施できる。
たとえばテストでHTTPリクエストがなされた場合必ず400エラーが返ってくるようになっており、モックを利用したテストが必須となっている。
以下、HTTPリクエストを例にmockの利用を見る。
mockの作成
import 'package:http/http.dart' as http;
import 'package:mockito/annotations.dart';
([MockSpec<http.Client>()])
void main() {}
build_runnnerでmockを自動で作成する。
flutter pub run build_runner build
テストするコード
import 'package:http/http.dart' as http;
Future<bool> isUrlExists(String? url, {http.Client? client}) async {
client ??= http.Client(); // テスト時はmockのHttpClientを使う
final getResponse = await client.get(
Uri.parse(url),
);
logger.i('GET request status code: ${getResponse.statusCode}');
if (getResponse.statusCode == 200) {
return true;
}
return false;
}
テストコード
group('isUrlExist関数', () {
fina url = Uri.parse('https://example.com');
test('GETリクエストが成功', () async {
when(mockClient.get(url)).thenAnswer((_) async => http.Response('', 200));
final result = await isUrlExists(url.toString(), client: mockClient);
expect(result, isTrue);
});
test('URLが存在しない場合 (GETリクエストが失敗)', () async {
when(mockClient.get(url)).thenAnswer((_) async => http.Response('', 404));
final result = await isUrlExists(url.toString(), client: mockClient);
expect(result, isFalse);
// モックメソッドの呼び出しを検証
verify(client.get(url)).called(1);
});
});
verify
mockが期待通りに呼び出されたかどうかを確認する。
何回呼び出されたか、どのような引数で呼び出されたかを検証するのに役立つ。
ウィジェットテストで使う主な手法
pumpWidget
WidgetTesterクラスのメソッド
まず最初にテストするウィジェットを立ちあげるのがpumpWidget。
await tester.pumpWidget(MaterialApp(home:Scaffold(body: Widget,)));
その他よく使うpump系関数
// ウィジェットツリーの状態をすぐに更新する
// ウィジェットの初期状態や、イベントが発生した直後の状態を確認する際に使われる
// 引数にdurationを設定して待つこともできる
//
await tester.pump();
// 主にアニメーションなど時間がかかる処理を待つのに使う
// デフォルトでdurationが100msに設定されている
await tester.pumpAndSettle();
スナックバーの表示など、pumpAndSettleでもエラーになることがある。その時は時間をちゃんと設定する。
Finder
find.text('見つけたい文字列');
find.widgetWithText(特定のWidget,'見つけたい文字列');
find.byKey(key); // keyを付けると見つけやすいかも
find.byType(Widgetの型);
find.byIcon(Icon);
// カスタマイズできるよ
find.byWidgetPredicate(
(Widget widget) => widget is Tooltip && widget.message == 'Back',
description: 'with tooltip "Back"'
);
// ofで渡したWidgetの子Widgetで、matchingに合うWidgetを探す。
find.descendant(
of: find.byType(List),
matching: find.byType(見つけたいWidget),
)
find.ancestor(); // descendantの反対
見つけたいWidgetが複数見つかり特定できない時
// testWidgetsでウィジェットテストの準備済みの前提
// Row内のPaddingウィジェットを取得
final rowPaddingFinder = find.descendant(
of: find.byType(Row),
matching: find.byType(Padding),
);
// デバッグ出力で確認
final paddingWidgets =
tester.widgetList<Padding>(rowPaddingFinder).toList();
debugPrint('Row内のPaddingウィジェットの数: ${paddingWidgets.length}');
for (var padding in paddingWidgets) {
debugPrint('Paddingウィジェット: ${padding.padding}');
}
// 取得したいPaddingウィジェットを特定
final targetPadding = paddingWidgets
.firstWhere((p) => p.padding == const EdgeInsets.only(top: 2));
expect(targetPadding.padding, const EdgeInsets.only(top: 2));
resolve
// ボタンの背景色を設定
MaterialStateProperty<Color> backgroundColor = MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
// ボタンが押されたときの色
return Colors.red;
}
if (states.contains(MaterialState.hovered)) {
// ボタンがホバーされたときの色
return Colors.green;
}
// 通常の状態の色
return Colors.blue;
});
// 実際の色を取得するためにresolveを使います
// ボタンが押された状態の色を取得
Color pressedColor = backgroundColor.resolve({MaterialState.pressed});
// ボタンがホバーされた状態の色を取得
Color hoveredColor = backgroundColor.resolve({MaterialState.hovered});
// 通常の状態の色を取得
Color normalColor = backgroundColor.resolve({});
StatefulWidgetを使わずに状態の変化をテストする - ValueNotifier
StatelessWidgetだが渡されるisSendingの真偽によって表示が変わるボタンクラスをテストしたい。
final isSendingNotifier = ValueNotifier<bool>(false);
await tester.pumpWidget(
buildTestWidget(
child = ValueListenableBuilder<bool>(
valueListenable: isSendingNotifier,
builder: (context, isSending, child) {
return StyledButton(
'test button',
isSending: isSending,
onPressed: () {
// Notifierに値の変更を伝える
// isSending = true;ではダメ!
// isSendingはBuilder内のローカル変数
isSendingNotifier.value = true;
},
);
},
),
),
);
SvgPicture.assetのassetNameが取り出せない
SvgPicture.assetではassetNameつまりSvgファイルのパスを入力するのだが、その値をSvgPcture.asset.assetNameでは取り出せない(他のプロパティは取り出せるのに)。
SvgPicture SvgPicture.asset(
String assetName, {(以下引数略)
というのもSvgPictureクラスではbytesLoaderというリソースローダーに情報が詰め込まれており、それがSvgPictureクラスではBytesLoader型だが、SvgPicture.assetだとSvgAssetLoader型に変換されている。
// SvgPictureクラスでは
final BytesLoader bytesLoader;
// SvgPicture.assetだと
bytesLoader = SvgAssetLoader(
assetName,
packageName: package,
assetBundle: bundle,
theme: theme,
)
なので呼び出すために型変換も必要となる。
final svgPicture = tester.widget<SvgPicture>(find.byType(SvgPicture));
// 本来SvgPictureクラスのbytesLoaderはBytesLoader型だが
// SvgPicture.assetの場合はSvgAssetLoader型に変換される
final svgAssetLoader = svgPicture.bytesLoader as SvgAssetLoader;
expect(svgAssetLoader.assetName, imagePath);
依存オブジェクトの注入 - Dependency Injection
SupabaseCLIでローカル環境を構築しテストする
Riverpod(provider)のテスト
Providerにモックをどう渡すか
Supabaseからアーティストのリストを取得するプロバイダーがある。
そのプロバイダーに依存オブジェクトであるSupabaseとLoggerのモックを注入したい。
// テストコード
void main(List<String> args) {
group('artistListFromSupabaseProvider', () {
late final SupabaseClient mockSupabase;
late final MockSupabaseHttpClient mockHttpClient;
late final MockLogger mockLogger;
setUpAll(() {
mockHttpClient = MockSupabaseHttpClient();
mockSupabase = SupabaseClient(
'https://mock.supabase.co',
'fakeAnonKey',
httpClient: MockSupabaseHttpClient(),
);
mockLogger = MockLogger();
});
setUp(() async {
final artist = {
'id': 1,
'name': 'Artist 1',
'image_url': 'https://example.com/artist1.jpg',
'comment': 'Great artist',
};
await mockSupabase.from('artists').insert(artist);
resetMockitoState();
});
tearDown(() async {
mockHttpClient.reset();
});
test('Supabaseからデータを取得', () async {
// ProviderContainer
final container = createContainer(
overrides: [
loggerProvider.overrideWith((ref) => mockLogger),
supabaseProvider.overrideWith((ref) => mockSupabase),
],
);
final artists =
await container.read(artistListFromSupabaseProvider.future);
expect(artists.length, 1);
expect(artists.first.name, 'Artist 1');
verify(mockLogger.i('Supabaseからアーティストデータを取得中...')).called(1);
verify(mockLogger.i('1件のアーティストデータをSupabaseから取得しました')).called(1);
verify(mockLogger.i('1件のアーティストデータをリストにしました')).called(1);
// 忘れずコンテナは破棄する。そうしないと他のテストに影響してしまう。
container.dispose();
});
// 例外処理のテスト(略)
});
}
そのためにはまずSupabaseとLoggerのプロバイダーを作る。
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
final supabaseProvider = Provider<SupabaseClient>((ref) {
return supabase;
});
final supabase = Supabase.instance.client;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logger/logger.dart';
final loggerProvider = Provider<Logger>((ref) {
return logger;
});
final logger = Logger(
printer: PrettyPrinter(
lineLength: 70,
methodCount: 0,
errorMethodCount: 10,
),
);
テストで作ったProviderContainerでオーバーライドしたモックを渡すためには、渡したいオブジェクトのプロバイダーが必要となる。もともとsupabaseもloggerもプロバイダーを作らずやっていたから、ここの渡し方に苦戦した。
// テストするプロバイダー
final artistListFromSupabaseProvider =
FutureProvider<List<Artist>>((ref) async {
// 【重要】プロバイダーを作り、そこを通すことで
// ProviderContainerでオーバーライドしたモックを受け取ることができる
final logger = ref.read(loggerProvider);
final supabase = ref.read(supabaseProvider);
// fetchArtistList関数にモックを渡す
return fetchArtistList(supabase, logger);
});
例外処理はmockSupabaseとは別のSupabaseClient(errorSupabaseを名付けた)を作り同じように渡してやれば例外がスローされるのでテストできる。
ProviderContainer
ProviderContainerオブジェクトの作成と破棄のためのテストユーティリティを作成することが推奨されます。テスト間でのProviderContainerの共有はしてはいけません。
import 'package:riverpod/riverpod.dart';
import 'package:test/test.dart';
/// [ProviderContainer]を作成し、テスト終了時に自動破棄するテストユーティリティです。
ProviderContainer createContainer({
ProviderContainer? parent,
List<Override> overrides = const [],
List<ProviderObserver>? observers,
}) {
// ProviderContainerを作成し、オプションでパラメータを指定します。
final container = ProviderContainer(
parent: parent,
overrides: overrides,
observers: observers,
);
// テスト終了時、containerを破棄します。
addTearDown(container.dispose);
return container;
}
CI - GitHub Actionsにインテグレーションテストの導入
dev_dependencies:
flutter_test:
sdk: flutter
integration_test: # <- ここを追加
sdk: flutter # <- ここを追加
integration_testディレクトリを作り、テストファイルを作る。
アプリ自身を呼び出すため、main.dartを"as app"でインポートする。
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_project_app/main_app.dart';
# " as app "でインポート
import 'package:my_project_app/main.dart' as app;
void main() {
# インテグレーションテストで必須の一行
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('アプリの起動', (WidgetTester tester) async {
# " as app "でインポートしたので app.main()でアプリを起動できる
await app.main();
await tester.pumpAndSettle();
expect(find.byType(MainApp),findsOneWidget);
});
}
# アンドロイドエミュレータの環境を構築
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
# エミュレーターのキャッシュ
- name: Gradle cache
uses: gradle/actions/setup-gradle@v4
- name: AVD cache
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ matrix.api-level }}
- name: create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
script: echo "Generated AVD snapshot for caching."
インテグレーションテストを実施。
ユニットテストとインテグレーションテスト、それぞれのカバレッジファイルをひとつにまとめる。
- name: Unit Test
run: flutter test --coverage
- name: Integration Test
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
force-avd-creation: false
emulator-options: -no-snapshot -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: flutter test integration_test/main_test.dart --coverage --coverage-path=coverage/integration_lcov.info
# カバレッジファイルをマージするのに必要
- name: Install lcov
run: |
sudo apt-get update
sudo apt-get install lcov
- name: Merge coverage files
run: lcov -a coverage/lcov.info -a coverage/integration_lcov.info -o coverage/lcov.info
# カバレッジを表示してくれるサイト「Codecov」にlcov.infoファイルを送る
- name: Upload Code Coverage to Codecov
uses: codecov/codecov-action@v5
with:
file: ./coverage/lcov.info
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
slug: your_account
※ yamlでは" | "を最初に書くことで複数のコマンドが実行できる。
run: |
echo "command 1"
echo "command 2"
インテグレーションテスト
ロボットパターン
「テストの目的(What)を明確にし、テストの方法(How)を隠蔽することができます。これにより、テストコードが読みやすく、再利用しやすくなります」
実際にアプリケーションを使う際、私達人間はどのような操作をするか、それを代替するロボットを作りテストを行う感じか。
ubuntuで開発中のFlutterアプリをスマホ接続するとno permissions (user in plugdev group; are your udev rules wrong?)
スマホの画像を取り込む機能のテストをどうするか
image_pickerパッケージで実装しているスマホに保存されている画像を選択する機能をどうテストするか。そのままtester.tap()などとやると、スマホの画像フォルダが開いてしまいテストが難しい。そこでモックを作ることに。
- MethodChannelで、FlutterからAndroidのAPIを呼び出すチャネルを作る。
- チャネルへのメソッド呼び出しをモックするハンドラを設定する。
- 'pickImage'が呼び出されたことをチェックする。
- テスト画像を読み込み、一時ファイルパスを作成しリターンする。
Future<void> _setMockImagePicker() async {
// Androidと繋ぐチャネルを作る
const channel = MethodChannel('plugins.flutter.io/image_picker');
// モックの作成
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
(MethodCall methodCall) async {
// メソッドのチェック
if (methodCall.method == 'pickImage') {
// テスト用画像のパスを取得
final byteData = await rootBundle.load('assets/test.jpg');
final tempDir = await getTemporaryDirectory();
final tempPath = tempDir.path;
final file = File('$tempPath/test.jpg');
await file.writeAsBytes(
byteData.buffer
.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes),
);
return file.path;
}
return null;
},
);
}
画像選択する際にモックを用意してテストする。
Future<void> selectImage() async {
await _setMockImagePicker();
await ensureVisibleWidget(WidgetKeys.image);
await tapWidget(WidgetKeys.image);
await tester.pumpAndSettle();
}
パフォーマンステスト
応用的finder
find.descendent
リストや似たような構造の中から特定のウィジェットを見つけたいとき、descendantつまり子孫を見つけるfinderを使う。
matchingに目的の要素を。
ofに目的の要素の上位にある要素を。
find.descendant(
of: 祖先のFinder,
matching: 目的のFinder
)
find.byWidgetPredicate
boolを返す関数であるpredicate(述語)を引数に取る。非常に柔軟なFinderを作ることができる。
find.byWidgetPredicate(
(Widget widget) => widget is Tooltip && widget.message == 'Back',
)