概要
url_launcher を mockito でモック化し、指定のURLが開かれたことをウィジェットテストで検証しようと思い方法を探したところ下記の記事を見つけましたが、
これで対象としている canLaunch()
, launch()
は記事作成時点の最新版1 では非推奨2 なので
推奨の canLaunchUrl()
, launchUrl()
(以下「推奨API」と記します)で同様に検証する方法がないか探ったところ突き止めることができたので以下に紹介します。
環境
[√] Flutter (Channel stable, 3.24.4, on Microsoft Windows [Version 10.0.19045.5487], locale ja-JP)
[√] Windows Version (Installed version of Windows is version 10 or higher)
[√] Android toolchain - develop for Android devices (Android SDK version 33.0.1)
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.3.6)
[!] Android Studio (version 2021.3)
X Unable to determine bundled Java version.
[√] Android Studio (version 2024.1)
[√] VS Code (version 1.97.0)
[√] VS Code, 64-bit edition (version 1.64.2)
[√] Connected device (3 available)
[√] Network resources
url_launcher: ^6.3.1
dev_dependencies:
build_runner: ^2.4.13
mockito: ^5.4.4
test: ^1.25.7
(先に)結論
下記のコードで推奨APIが呼び出されたことの検証が可能でした。
class FakeUrlLauncherPlatform extends Fake
with MockPlatformInterfaceMixin
implements UrlLauncherPlatform {} // ここは冒頭参考記事と同じ
const linkUrl = 'https://www.hogehoge.com/';
// モックインスタンスの生成と挙動設定 setUp() 等で実行しても勿論OK
var mockUrlLauncherPlatform = MockFakeUrlLauncherPlatform();
UrlLauncherPlatform.instance = mockUrlLauncherPlatform;
when(mockUrlLauncherPlatform.canLaunch(linkUrl))
.thenAnswer((_) async => true); // <= ポイント1(後述)
when(mockUrlLauncherPlatform.launchUrl(linkUrl, any))
.thenAnswer((_) async => true); // <= ポイント2(後述)
// url_launcher が実行されるはずのコードをこの間に記載
// url_launcher(モック)が実行されたことの確認
verify(mockUrlLauncherPlatform.canLaunch(linkUrl)); // <= ポイント1(後述)
verify(mockUrlLauncherPlatform.launchUrl(linkUrl, any)); // <= ポイント2(後述)
解説
url_launcher (とそのモック)の裏側
build_runner で url_launcher_screen_test.mocks.dart を生成すると MockFakeUrlLauncherPlatform
クラスは下記メソッドを持っていることが分かります。3
メソッドのリスト(クリックで展開)
canLaunch(String? url)
launch(String? url, {
required bool? useSafariVC,
required bool? useWebView,
required bool? enableJavaScript,
required bool? enableDomStorage,
required bool? universalLinksOnly,
required Map<String, String>? headers,
String? webOnlyWindowName,
})
launchUrl(String? url, LaunchOptions? options)
closeWebView()
supportsMode(PreferredLaunchMode? mode)
supportsCloseForMode(PreferredLaunchMode? mode)
ただ、よく見てみると推奨APIとは引数 url
の型が違ったり(Uri
⇔String?
)、canLaunch Url () に至っては存在すらしません。
そのため MockFakeUrlLauncherPlatform
に対し推奨APIと同じ名前・引数で when
や verify
を設定してみてもコンパイルに失敗します。
そこで今度は推奨API自体を url_launcher のソースから探してみると
url_launcher_uri.dart に定義があり
内側では UrlLauncherPlatform.instance
の canLaunch()
4 launchUrl()
を実行していることが見て取れます。
テストでは UrlLauncherPlatform.instance
== MockFakeUrlLauncherPlatform
なので、これがどう実行されているか読み解くことでモック化・呼出検証の方法が分かりました。
テスト実装時のポイント
以上を踏まえ、テストコード例中のポイントを補記すると
-
canLaunchUrl()
は内部では非推奨のAPIと変わらずUrlLauncherPlatform.instance.canLaunch()
を実行しているので、推奨APIcanLaunchUrl()
でプロダクトを実装していても
テストでのモック呼出検証は冒頭の参考記事と全く同様にcanLaunch()
に対しwhen
・verify
を行えばOKでした。 - 内部で実際に叩かれる
UrlLauncherPlatform.instance.launchUrl()
の第2引数options
は
推奨APIのlaunchUrl()
にはない上に nullable なのでうっかり省略したくなりますが、略すとwhen
・verify
ではコンパイルが通らないので何らかの指定が必要です。
とは言え単純に url_launcher で指定のURLが開かれたことを検証したいだけなら特定の値を指定する必要がないのでany
としておけば問題ありません5。
テストコード全文
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
import 'url_launcher_screen.dart'; // テスト対象画面(詳細コードは割愛)
import 'url_launcher_screen_test.mocks.dart';
class FakeUrlLauncherPlatform extends Fake
with MockPlatformInterfaceMixin
implements UrlLauncherPlatform {}
@GenerateNiceMocks([MockSpec<FakeUrlLauncherPlatform>()]) //★(下方に補足あり)
void main() {
const linkUrl = 'https://www.hogehoge.com/';
late MockFakeUrlLauncherPlatform mockUrlLauncherPlatform;
setUp(() {
// モックインスタンスの生成と挙動設定
mockUrlLauncherPlatform = MockFakeUrlLauncherPlatform();
UrlLauncherPlatform.instance = mockUrlLauncherPlatform;
when(mockUrlLauncherPlatform.canLaunch(linkUrl))
.thenAnswer((_) async => true); // <= ポイント1
when(mockUrlLauncherPlatform.launchUrl(linkUrl, any))
.thenAnswer((_) async => true); // <= ポイント2
});
testWidgets('Testing Open In Browser', (tester) async {
// URLをurl_launcherで開くボタン/リンクを持つテスト対象画面のロード
await tester.pumpWidget(const MaterialApp(home: UrlLauncherScreen(url: linkUrl)));
// url_launcherを実行するアクションを実行(この場合アイコンボタンをタップ)
await tester.tap(find.byIcon(Icons.open_in_browser));
await tester.pump();
// url_launcher が実行されたことの確認
verify(mockUrlLauncherPlatform.canLaunch(linkUrl)); // <= ポイント1
verify(mockUrlLauncherPlatform.launchUrl(linkUrl, any)); // <= ポイント2
});
}
★:冒頭参考記事では @GenerateMocks
annotation でモックを generate しているところ
現行 mockito の推奨 annotation である @GenerateNiceMocks
を用いても問題なかったのでその旨も付記しておきます。