TOP(About this memo)) > 一覧(Flutter) > テスト
flutter test
で実行される テストのうち、単一の関数、メソッド、またはクラスを対象とするもの。flutter test
で実行される テストのうち、testWidgets()を利用するもの。flutter test integration_test/xxx
で実行するもの。検証可能なもの | 検証ができない・難しいもの | |
---|---|---|
ウィジェットテスト | ・ウィジェット ・明示的なフレーム進行 ・FakeAsyncで進行させる非同期処理(モック化) |
・HttpClientで実際のデータを取得する処理 ・現実の時間軸に沿ったフレーム進行 ・現実の時間軸に沿った非同期処理 ・ネイティブ側のAPIを呼び出す処理(プラグインの処理) |
結合テスト | ・HttpClientで実際のデータを取得する処理 ・現実の時間軸に沿ったフレーム進行 ・現実の時間軸に沿った非同期処理 ・ネイティブ側のAPIを呼び出す処理(プラグインの処理) |
・ネイティブ側のUIを操作する処理 ・ネイティブ側のAPIの処理のエッジケース |
flutter pub add 'dev:flutter_test:{"sdk":"flutter"}' 'dev:integration_test:{"sdk":"flutter"}'
integration_test:
sdk: flutter
If you're running the tests with `flutter drive`, please make sure your tests
are in the `integration_test/` directory of your package and use
`flutter test $path_to_test` to run it instead.
flutter test -h
で確認flutter test
はデフォルトでデバッグモードのため--debug
オプションは存在しないflutter test
Test directory "test" not found.
と表示されエラーとなる。flutter test tests/xxx_test.dart
flutter test --plain-name 'widget'
test("widget test", () async {}); // 実行される
test("my widget", () async {}); // 実行される
test("test", () async {}); // 実行されない
group("widget", () {
testWidgets(
// 実行される
'hello1',
(_) async {},
);
testWidgets(
// 実行される
'hello2',
(_) async {},
);
});
for (int i = 1; i < 3; i++) {
testWidgets(
'widget $i',
(WidgetTester tester) async {
print(i);
},
);
}
flutter test
では成功するが –test-randomize-ordering-seed=シード値 によって順番をシャッフルすると失敗することが確認できる。int var1 = 0;
void main() {
for (int i = 0; i < 3; i++) {
test(
'MyTest$i',
() {
expect(i, var1);
print(var1++);
},
);
}
}
/*
00:01 +0: MyTest0
0
00:01 +1: MyTest1
1
00:01 +2: MyTest2
2
*/
integration_testプラグインを追加した状態でflutter test
において、integration_test
またはその配下のファイルを指定するとデフォルトのデバイスに対してビルドから実行される。
-d (デバイスID)
にて指定する。integration_test
以外の ディレクトリ名を指定すると、通常のflutter test
で対象を指定した際の挙動となる。
全テストを実行
flutter test integration_test -d 'iPhone 15'
flutter test integration_test -d 'chrome'
Web devices are not supported for integration tests yet.
と表示され失敗する。個別に実行
flutter test -d "対象のデバイスID" integration_test/xxx_test.dart
flutter run -d "対象のデバイスID" integration_test/xxx_test.dart
flutter run
で実行する方が早い。flutter run
で複数回リスタート・ホットリロードをしているとクラッシュすることがあるため、そういった場合は再度立ち上げる。IntegrationTestWidgetsFlutterBinding.ensureInitialized()
flutter test integration_test
において自動的にこのメソッドがmain処理の前に暗黙的に実行される。
flutter run
の場合は暗黙的には呼ばれない。brew install lcov
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
flutter test --coverage && lcov --remove coverage/lcov.info lib/main.dart -o coverage/lcov.info
// test/coverage_dummy_test.dart
// ignore: unused_import
import 'package:sample/main.dart';
void main() {
// Fake test in order to make each file reachable by the coverage
}
int var1 = 0;
int var2 = 0;
void main() {
setUpAll(() {
print("setUpAll");
});
setUp(() {
print("setUp");
var1 = 0;
});
tearDownAll(() {
print("tearDownAll");
});
tearDown(() {
print("tearDown");
});
group("MyGroup", () {
for (int i = 0; i < 3; i++) {
testWidgets(
'MyTest$i',
(WidgetTester tester) async {
print("var1:${var1++}, var2:${var2++}");
},
);
}
});
}
/*
flutter: setUpAll
00:01 +0: (setUpAll)
setUpAll
00:01 +0: MyGroup MyTest0
setUp
var1:0, var2:0
tearDown
00:01 +1: MyGroup MyTest1
setUp
var1:0, var2:1
tearDown
00:01 +2: MyGroup MyTest2
setUp
var1:0, var2:2
tearDown
00:01 +3: (tearDownAll)
tearDownAll
*/
https://api.flutter.dev/flutter/flutter_test/testWidgets.html
Runs the callback inside the Flutter test environment.
testWidgets()を実行時、渡したコールバックの実行開始時点で シングルトンのTestWidgetsFlutterBinding.instanceには必ずインスタンスが設定されている。
ここで設定されるTestWidgetsFlutterBindingのruntimeTypeは実行条件によって異なる。
実行条件(起動コマンド, ソースコード) | TestWidgetsFlutterBinding.instance のruntimeType | |
---|---|---|
No.1 | flutter test | AutomatedTestWidgetsFlutterBinding |
No.2 | flutter run | LiveTestWidgetsFlutterBinding |
No.3 | flutter test integrate_test | IntegrationTestWidgetsFlutterBinding |
No.4 | flutter run integrate_test | LiveTestWidgetsFlutterBinding |
No.5 | IntegrationTestWidgetsFlutterBinding.ensureInitialized() を実行 |
IntegrationTestWidgetsFlutterBinding |
サンプルコード
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
main() {
// 以下を実行した場合はすべてIntegrationTestWidgetsFlutterBindingとなる。
// IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets("", (widgetTester) async {
debugPrint(TestWidgetsFlutterBinding.instance.toString());
});
}
// ・flutter testで実行
// <AutomatedTestWidgetsFlutterBinding>
// ・flutter runで実行
// <LiveTestWidgetsFlutterBinding>
// ・flutter test integration_test で実行(flutter test integration_test/xxx_test.dart でも同様)
// <IntegrationTestWidgetsFlutterBinding>
import 'dart:convert';
import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('', (tester) async {
final httpClient = HttpClient();
final request = await httpClient.getUrl(Uri.parse("https://example.com"));
final res = await request.close();
final responseBody = await res.transform(utf8.decoder).join();
debugPrint(responseBody);// ""
debugPrint(res.statusCode.toString());// 400
});
}
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('', (tester) async {
await tester.pumpWidget(const CircleAvatar(
backgroundImage:
NetworkImage("https://docs.flutter.dev/assets/images/dash/Dash.png"),
));
});
}
/*
══╡ EXCEPTION CAUGHT BY IMAGE RESOURCE SERVICE ╞════════════════════════════════════════════════════
The following NetworkImageLoadException was thrown resolving an image codec:
HTTP request failed, statusCode: 400, https://docs.flutter.dev/assets/images/dash/Dash.png
...
*/
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets(/* 上記の例と同じ処理 */);
}
// (integration_test/)
// 00:01 +1: All tests passed!
The current time.In the automated test environment (flutter test), this is a fake clock that begins in January 2015 at the start of the test and advances each time pump is called with a non-zero duration.In the live testing environment (flutter run), this object shows the actual current wall-clock time.
Unit tests run with flutter test run inside a headless flutter shell on your workstation, you won’t see any UI.
-d "iPhone 15"
のようにデバイスIDを指定しても無視される。Start in a paused mode and wait for a debugger to connect. You must specify a single test file to run, explicitly. Instructions for connecting with a debugger are printed to the console once the test has started.
Size(800.0, 600.0)
が適用される。main() {
testWidgets("", (widgetTester) async {
await widgetTester.pumpWidget(MaterialApp(
home: Column(
children: [
const Row(
children: [Text("my text1")],
),
const Row(
children: [Text("my text2")],
),
TextButton(onPressed: () {}, child: const Text("my text3")),
ElevatedButton(onPressed: () {}, child: const Text("my text3")),
const CircleAvatar(
child: Icon(Icons.people),
),
const CircleAvatar(
backgroundColor: Colors.red,
child: Icon(Icons.question_mark),
),
],
),
));
expect(
find.descendant(
of: find.byType(MaterialApp), matching: find.byType(Text)),
findsExactly(4));
expect(find.ancestor(of: find.byType(Text), matching: find.byType(Row)),
findsExactly(2));
expect(find.ancestor(of: find.text("my text1"), matching: find.byType(Row)),
findsOne);
expect(find.textContaining("my text"), findsExactly(4));
expect(find.widgetWithText(ElevatedButton, "my text3"), findsOne);
expect(find.widgetWithIcon(CircleAvatar, Icons.people), findsOne);
expect(find.byWidgetPredicate((widget) => widget is CircleAvatar && widget.backgroundColor == Colors.red), findsOne);
});
}
ScrollController? getScrollController() {
final finder = find.byType(ListView);
if (finder.evaluate().isEmpty) return null;
final listViewWidget = finder.evaluate().first.widget as ListView;
return listViewWidget.controller;
}
find.byType(SomeWidget)
は SomeWidget<dynamic>にヒットするがSomeWidget<int>にはヒットしないPageView(children: [
PageA(),// Scrollableを含む
PageB()
])
final pageViewScrollable = find.descendant(
of: find.byType(PageView),
matching: find.byWidgetPredicate((widget) =>
widget is Scrollable &&
widget.axisDirection == AxisDirection.right));
// flutter(3.24.3): packages/flutter/test/material/outlined_button_test.dart
final Finder outlinedButton = find.byType(OutlinedButton);
//...
expect(outlinedButton, paints..drrect(color: defaultColor));
// flutter(3.24.3): packages/flutter/test/material/range_slider_test.dart
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
//...
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
// flutter(3.24.3): packages/flutter/test/material/app_bar_test.dart
final RenderObject painter = tester.renderObject(
find.descendant(
of: find.descendant(
of: find.byType(AppBar),
matching: find.byType(Stack),
),
matching: find.byType(Material),
),
);
//...
expect(painter, paints..save()..translate()..save()..translate()..circle(x: 24.0, y: 28.0));
// flutter(3.24.3): packages/flutter/test/painting/circle_border_test.dart
const CircleBorder c10 = CircleBorder(side: BorderSide(width: 10.0));
//...
expect(
(Canvas canvas) => c10.paint(canvas, const Rect.fromLTWH(10.0, 20.0, 30.0, 40.0)),
paints
..circle(x: 25.0, y: 40.0, radius: 10.0, strokeWidth: 10.0),
);
void main() {
testWidgets("", (widgetTester) async {
Future.microtask(() => print("microtask1"));
Future.microtask(() => print("microtask2"));
Future(() => print("future"));
Timer.run(() => print("timer run"));
print("not yet future and microtask done");
widgetTester.pump(Duration.zero);// durationを指定しないとFuture(※ 内部ではTimer.runを実行)は進まない。
});
/*
not yet future and microtask done
microtask1
microtask2
future
timer run
*/
}
import 'dart:convert';
import 'dart:io';
class OriginalClass {
Future<int> getStubData() async {
final request = await HttpClient().getUrl(Uri.parse("https://example.com"));
final res = await request.close();
final json = jsonDecode(await res.transform(utf8.decoder).join());
return json["value"];
}
}
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sample/original.dart';
void main() {
testWidgets('', (tester) async {
Stub().getStubData();
debugPrint("get data started");
await tester.pump(const Duration(milliseconds: 499));
debugPrint("wait..");
await tester.pump(const Duration(milliseconds: 1));
});
}
class Stub extends OriginalClass {
@override
Future<int> getStubData() async {
await Future.delayed(const Duration(milliseconds: 500));
debugPrint("get data finished");
return 1;
}
}
/*
get data started
wait..
get data finished
*/
flutter run
で実行した際は、下記のようになる。(環境によっては”flutter: wait..“の方が先になるかもしれない?)flutter: get data started
flutter: get data finished
flutter: wait..
// 参照元:
// https://github.com/flutter/flutter/issues/73355#issuecomment-805736745
// 元のコードはintegration_testではない`flutter test`で利用した場合にExceptionは出るがwhileが終了しなかったため、少し改変をしている。
extension WidgetTesterExtension on WidgetTester {
Future<void> pumpUntilFound(
Finder finder, {
Duration? duration,// フェイクのクロックの際に使う場合は、durationを指定する
Duration timeout = const Duration(seconds: 10),
}) async {
var found = false;
var failed = false;
final timer = Timer(
timeout,
() {
failed = true;
},
);
while (!found) {
if (failed) {
throw TimeoutException('Pump until has timed out $finder');
}
await pump(duration);
found = any(finder);
}
timer.cancel();
}
}
main() {
testWidgets("", (widgetTester) async {
final textFieldController = TextEditingController();
String message = "";
await widgetTester.pumpWidget(MaterialApp(
home: Material(
child: Row(
children: [
Expanded(
child: TextField(
controller: textFieldController,
)),
IconButton(
icon: const Icon(Icons.send),
onPressed: () {
message = textFieldController.text;
},
),
],
)),
));
const inputText = "input text";
await widgetTester.enterText(find.byType(TextField), inputText);
await widgetTester.tap(find.widgetWithIcon(IconButton, Icons.send));
expect(message, inputText);
});
}
BuildContext getContext(WidgetTester widgetTester, [Type? type]) {
return widgetTester.element(find.byType(type ?? Scaffold));
}
// flutter(3.24.3) examples/api/test/material/theme/theme_extension.1_test.dart
final ThemeData theme = Theme.of(
tester.element(find.byType(example.Home)),
);
testWidgets("", (widgetTester) async {
final navigatorKey = GlobalKey<NavigatorState>();
await widgetTester.pumpWidget(MaterialApp.router(
routerConfig: GoRouter(navigatorKey: navigatorKey, routes: [
GoRoute(
path: "/",
builder: (context, state) => Scaffold(
appBar: AppBar(
title: const Text("home title"),
),
),
),
GoRoute(
path: "/a",
builder: (context, state) => Scaffold(
appBar: AppBar(
title: const Text("a title"),
),
),
)
]),
));
GoRouter.of(navigatorKey.currentState!.context).go("/a");
// 単にフレームを進めても表示されない。
// await widgetTester.pump();
expect(find.text("a title"), findsNothing);
// pumpAndSettleはdurationがデフォルトで指定されるためフェイクのクロックが進む
await widgetTester.pumpAndSettle();
expect(find.text("a title"), findsOne);
});
testWidgets('SnackBar control test', (WidgetTester tester) async {
const String helloSnackBar = 'Hello SnackBar';
GlobalKey key = GlobalKey();
await tester.pumpWidget(MaterialApp(
home: Scaffold(key: key),
));
expect(find.text(helloSnackBar), findsNothing);
ScaffoldMessenger.of(key.currentContext!).showSnackBar(const SnackBar(
duration: Duration(seconds: 2), content: Text(helloSnackBar)));
expect(find.text(helloSnackBar), findsNothing);
await tester.pump(); // schedule, begin animation(show)
expect(find.text(helloSnackBar), findsOneWidget);
await tester
.pump(const Duration(milliseconds: 260)); // progress animation(show)
expect(find.text(helloSnackBar), findsOneWidget);
await tester.pump(const Duration(
milliseconds: 2000)); // display, schedule animation(dismiss)
expect(find.text(helloSnackBar), findsOneWidget);
await tester.pump(const Duration(
milliseconds: 260)); // begin, progress animation(dismiss)
expect(find.text(helloSnackBar), findsNothing);
});
expect(find.text("スナックバー表示テキスト"), findsOneWidget);
とした際にScaffoldを含む画面がスタックに複数存在する場合は、findsOneWidget
ではなく画面数分設定する必要がある。testWidgets("", (tester) async {
await tester.pumpWidget(MaterialApp(
home: IconButton(
onPressed: () => throw "error", icon: const Icon(Icons.abc))));
await tester.tap(find.widgetWithIcon(IconButton, Icons.abc));
expect(tester.takeException().toString(), contains("error"));
});
Future<void> pageBack(WidgetTester tester) async {
// 現在のlocaleに対応するページバックボタンのツールチップの文字列を取得する方法が分からなかったため、
// flutter_localizationsの設定ファイル(app_en.arb)にハードコードしてそれを参照する。
// (テストの際はロケーションはenがデフォルトとなる)
Finder backButton =
find.byTooltip(AppLocalizations.of(context)!.appbarBackTooltip);
expect(backButton, findsOneWidget);
await tester.tap(backButton);
await tester.pumpAndSettle();
}
//lib/l10n/app_en.arb
{
"@@locale":"en",
"appbarBackTooltip": "Back",
//〜〜
}
末端へ移動
Future<void> scrollJumpToEndAndPump(WidgetTester widgetTester) async {
final finder = find.byType(ListView);
final listViewWidget = finder.evaluate().first.widget as ListView;
listViewWidget.controller!.jumpTo(listViewWidget.controller!.position.maxScrollExtent);
await widgetTester.pump();
}
WidgetTester.drag
WidgetTester.dragUntilVisible, scrollUntilVisible
refresyIndicatorのアクションを発生させる
Future<void> refreshActionOnScrollable(WidgetTester tester,
{FinderBase<Element>? scrollable,
Offset delta = const Offset(0, 400),
double speed = 100,
Duration? pumpAndSettleDuration = const Duration(milliseconds: 1)}) async {
await tester.fling(scrollable ?? find.byType(Scrollable), delta, speed);
if (pumpAndSettleDuration != null) {
await tester.pumpAndSettle(pumpAndSettleDuration);
}
}
void main() {
testWidgets('', (tester) async {
Future.delayed(const Duration(milliseconds: 500));
});
}
/*
Pending timers:
Timer (duration: 0:00:00.500000, periodic: false), created:
#0 new FakeTimer._ (package:fake_async/fake_async.dart:308:62)
...
*/
void main() {
testWidgets('', (tester) async {
Future.delayed(const Duration(milliseconds: 500));
await tester.binding.delayed(const Duration(days: 1));
});
}
expect(find.byType(Scaffold), matchesGoldenFile('./golden/xxx.png'));
flutter test --update-goldens
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
// flutter test --plain-name 'asset test'
void main() {
group("asset test", () {
for (int i = 1; i <= 3; i++) {
testWidgets(i.toString(), (widgetTester) async {
await widgetTester.pumpWidget(const MaterialApp(
home: Scaffold(
body: Center(
child: CircleAvatar(
radius: 100,
backgroundImage: AssetImage('assets/Dash.png'),
),
),
)));
await cacheCircleAvatorImage(widgetTester);
await expectLater(
find.byType(Scaffold),
matchesGoldenFile(
"../golden/asset_test/${widgetTester.testDescription}.png"));
});
}
});
}
Future<void> cacheCircleAvatorImage(WidgetTester widgetTester) async {
// 確実にイメージが表示されるように、ImageProviderをキャッシュしておく。
await widgetTester.runAsync(() async {
// https://github.com/flutter/flutter/issues/38997#issuecomment-555687558
for (var element in find.byType(CircleAvatar).evaluate()) {
// print(element);
final CircleAvatar widget = element.widget as CircleAvatar;
final ImageProvider? image = widget.backgroundImage;
if (image != null) {
await precacheImage(image, element);
}
// 上記を実行するとrenderObject.debugNeedsPaint が trueになるためフレームを進める必要がある。
await widgetTester.pump();
}
});
}
void main() {
group("asset test", () {
testWidgets("test", (tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: MyWidget(),
)));
cat = true;
(tester.element(find.byType(MyWidget)) as StatefulElement)
.state
// ignore:invalid_use_of_protected_member
.setState(() {});
// pumpとpumpAndSettlerのどちらを実行するかによって後続のゴールデンテストで生成されるファイルが異なる。
//await tester.pump();
await tester.pumpAndSettle();
await cacheCircleAvatorImage(tester);
// pumpの場合: ゴールデンテストのイメージはcatに変わっていない
// pumpAndSettleの場合: ゴールデンテストのイメージはcatに変わっている
await expectLater(find.byType(Scaffold),
matchesGoldenFile("./golden/test/${tester.testDescription}1.png"));
});
});
}
bool cat = false;
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
@override
Widget build(BuildContext context) {
return CircleAvatar(
backgroundImage: cat
? const AssetImage('assets/cat.png')
: const AssetImage('assets/Dash.png'),
);
}
}
//...
CircleAvatar(
backgroundImage: MyImage.getImage(url),
)
//...
class MyImageStub extends MyImage {
ImageProvider<Object> getImage(String url) {
return MemoryImage(base64Decode('R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='));
}
}
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
// 参考
// flutter/test/painting/image_provider_network_image_test.dart
// flutter_test/lib/src/_binding_io.dart
class _FakeHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
return _MockHttpClient();
}
}
class _MockHttpClient extends Fake implements HttpClient {
@override
bool autoUncompress = true;
@override
Future<HttpClientRequest> get(String host, int port, String path) {
return Future<HttpClientRequest>.value(_FakeHttpClientRequest());
}
@override
Future<HttpClientRequest> getUrl(Uri url) {
return Future<HttpClientRequest>.value(_FakeHttpClientRequest());
}
}
class _FakeHttpClientRequest extends Fake implements HttpClientRequest {
final _FakeHttpClientResponse response = _FakeHttpClientResponse();
@override
Future<HttpClientResponse> close() async {
return response;
}
}
class _FakeHttpClientResponse extends Fake implements HttpClientResponse {
bool drained = false;
@override
int statusCode = HttpStatus.ok;
@override
int contentLength = 1;
@override
HttpClientResponseCompressionState get compressionState =>
HttpClientResponseCompressionState.notCompressed;
@override
StreamSubscription<List<int>> listen(
void Function(List<int> event)? onData, {
Function? onError,
void Function()? onDone,
bool? cancelOnError,
}) {
return Stream<List<int>>.fromIterable(<Uint8List>[
// 透明な1pixelドット
base64Decode('R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='),
]).listen(
onData,
onDone: onDone,
onError: onError,
cancelOnError: cancelOnError,
);
}
@override
Future<E> drain<E>([E? futureValue]) async {
drained = true;
return futureValue ??
futureValue as E; // Mirrors the implementation in Stream.
}
}
void main() {
group('', () {
setUpAll(() {
HttpOverrides.global = _FakeHttpOverrides();
// HttpOverrides.globalをnullにすると、モックによるhttpリクエスト自体の失敗は発生しないが、
// 通常のHttpClientが利用されてしまう為、推奨しない。
// このコードの場合は、実行してみるとペンディングのtimerが残っているというエラーになる
//(詳細は確認していないが、AutomatedTestWidgetsFlutterBindingのテスト内でHttpClientをテストで実行することは避けた方が良いだろう。)
// HttpOverrides.global = null;
// debugNetworkImageHttpClientProviderを上書きする方法でも動作する。
// ただし、テストの終了時にdebugNetworkImageHttpClientProvider = nullを実行する必要があり、実行しない場合はassertエラーとなる。
// (仕様上、tearDownではなく都度テストの最後に記述する必要がある。)
// debugNetworkImageHttpClientProvider = _FakeHttpClient.new;
});
testWidgets('', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: CircleAvatar(
backgroundImage: NetworkImage('https://example.com/dummy.svg'),
),
),
);
await expectLater(
find.byType(MaterialApp),
matchesGoldenFile(
'./test1.png',
),
);
});
});
}
class Backend {
Backend({
required this.httpGet,
required this.endpoint,
});
final String endpoint;
final Future<(int statusCode, String responseBody)> Function(
Uri url,
) httpGet;
Future<(T, int)> request<T>(
String path, T Function(Map<String, dynamic> json) fromJson) async {
// ステータスコードやエラーのハンドリング等は省略
late final int statusCode;
late final String responseBody;
late final Map<String, dynamic> json;
(statusCode, responseBody) = await httpGet(Uri.parse("$endpoint/$path"));
json = jsonDecode(responseBody);
return (fromJson(json), statusCode);
}
}
Future<(int statusCode, String body)> get(
Uri url, Map<String, dynamic>? body) async {
late String responseBody;
late HttpClientResponse res;
final httpClient = HttpClient();
HttpClientRequest request;
request = await httpClient.getUrl(url);
request.headers.contentType = ContentType("application", "json");
res = await request.close();
responseBody = await res.transform(utf8.decoder).join();
return (res.statusCode, responseBody);
}
test("", () async{
final backend = Backend(
httpGet: (_) async {
return (200, '{"result":"success", "data":""}');
},
endpoint: "https://example.com");
final result = await backend.request("/test", (json) {
return json["result"] as String;
});
expect(result.$1, equals("success"));
});
Backend backend = Backend(/* 省略 */);
class Backend {
// 省略
}
backend = BackendStub(/* 省略 */);
class BackendStub extends Backend {
// 省略
}
setUpAll(() {
backend = ackendStub(/* 省略 */);
});
import 'dart:io';
import 'package:firebase_storage/firebase_storage.dart';
class FirebaseStorageClient {
// coverage:ignore-start
Future<String> uploadStorageFile(File file, String storagePath) async {
final storageRef = FirebaseStorage.instance.ref().child(storagePath);
await storageRef.putFile(file);
final downloadUrl = await storageRef.getDownloadURL();
return downloadUrl;
}
// coverage:ignore-end
}
import 'dart:io';
import 'package:sample/storage.dart';
class FirebaseStorageClientStub extends FirebaseStorageClient {
FirebaseStorageClientStub();
@override
Future<String> uploadStorageFile(File file, String storagePath) async {
return "dummy download url";
}
}
/Users/xxx/flutter/bin/cache/artifacts/engine/darwin-x64/flutter_tester --vm-service-port=0 --start-paused --enable-checked-mode --verify-entry-points --enable-software-rendering --skia-deterministic-rendering --enable-dart-profiling --non-interactive --use-test-fonts --disable-asset-fonts --packages=/Users/path/to/project/.dart_tool/package_config.json --flutter-assets-dir=/Users/Users/path/to/project/build/unit_test_assets /var/folders/tv/xxxxxxxxxx/T/flutter_tools.xxxxxxxxxx/flutter_test_listener.xxxxxxxxxx/listener.dart.dill
ps aux | grep "flutter_tester --vm-service-port=0" | grep -v grep | awk '{print $2}' | xargs kill -9
ローカルネットワーク上のデバイスの検索および接続を求めています
のダイアログによって結合テストがハングする。
Bad state: Can't call test() once tests have begun running
// lib/some_library.dart
//...
@visibleForTesting
void innerLogic() {
//...
}
// lib/another_library.dart
//...
// アナライザーによって警告が表示される
// The member 'innerLogic' can only be used within 'package:sample/main.dart' or a test.
innerLogic();
// test/some_library_test.dart
//...
innerLogic();//警告は発生しない
The value of ErrorWidget.builder was changed by the test
void main() {
// testWidgetsの外に書いても上書きされてしまうため適用されない。
// FlutterError.onError = (errorDetails) {};
// testWidgetsの外に書く。
ErrorWidget.builder = (_) => const Text("my error widget");
testWidgets("setErrorWidget", (tester) async {
FlutterError.onError = (errorDetails) {};
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: _MyApp(),
),
));
expect(find.text("my error widget"), findsOne);
});
}
class _MyApp extends StatelessWidget {
const _MyApp();
@override
Widget build(BuildContext context) {
throw Exception("error");
}
}
import 'package:flutter_test/flutter_test.dart';
main() {
testWidgets("", (widgetTester) async {
f();
f();
// エラー:
// Guarded function conflict.
// You must use "await" with all Future-returning test APIs.
// The guarded "f" function was called from
});
}
Future<String> f() async {
return await TestAsyncUtils.guard<String>(() async => "dummy text");
}
flutter test
における testWidgets()の処理flutter test
で実行した際の処理の流れ、および関係するクラスとなる。TestWidgetsFlutterBinding._instance
flutter test
もしくはweb環境の場合は TestWidgetsFlutterBinding._instanceには AutomatedTestWidgetsFlutterBindingのインスタンスが設定される。TestWidgetsFlutterBinding.overrideHttpClient
AutomatedTestWidgetsFlutterBinding.runTest()
_clock = fakeAsync.getClock(DateTime.utc(2015));
として2015年の日時を格納している。
AutomatedTestWidgetsFlutterBinding.pump()
flutter test
の場合は、これが無いためpump()によって能動的に実行する。TestWidgetsFlutterBinding get binding => super.binding as TestWidgetsFlutterBinding;
にて TestWidgetsFlutterBindingまたはその派生オブジェクトの各メソッドを呼ぶ。
TestWidgetsFlutterBinding._instance
flutter run
にて実行した場合は TestWidgetsFlutterBinding._instanceには LiveTestWidgetsFlutterBindingが設定される。LiveTestWidgetsFlutterBinding.pump()
IntegrationTestWidgetsFlutterBinding.ensureInitialized()
IntegrationTestWidgetsFlutterBinding.ensureInitialized()
を(testWidgets()の実行より前に)実行する場合はIntegrationTestWidgetsFlutterBindingが_instanceに設定される。IntegrationTestWidgetsFlutterBinding.overrideHttpClient
HttpOverrides.global
// flutter/bin/cache/pkg/sky_engine/lib/_http/http.dart
factory HttpClient({SecurityContext? context}) {
HttpOverrides? overrides = HttpOverrides.current;
if (overrides == null) {
return _HttpClient(context);
}
return overrides.createHttpClient(context);
}
flutter test
の場合は何らかのダミーのFlutterViewが設定されると推測している。const Size _kDefaultTestViewportSize = Size(800.0, 600.0);
//...
class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
//...
@override
ViewConfiguration createViewConfigurationFor(RenderView renderView) {
final FlutterView view = renderView.flutterView;
if (view == platformDispatcher.implicitView) {
return TestViewConfiguration.fromView(
size: _surfaceSize ?? _kDefaultTestViewportSize,
view: view,
);
}
// ...
}
//...
class IntegrationTestWidgetsFlutterBinding extends LiveTestWidgetsFlutterBinding implements IntegrationTestResults {
//...
@override
ViewConfiguration createViewConfigurationFor(RenderView renderView) {
final FlutterView view = renderView.flutterView;
final Size? surfaceSize = view == platformDispatcher.implicitView ? _surfaceSize : null;
return TestViewConfiguration.fromView(
size: surfaceSize ?? view.physicalSize / view.devicePixelRatio,
view: view,
);
}
//...
}
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
はどこから呼ばれるのかflutter test integration_test
で実行する場合は暗黙的に IntegrationTestWidgetsFlutterBinding.ensureInitialized();
が実行されている。#0 debugPrintStack (package:flutter/src/foundation/assertions.dart:1201:29)
#1 IntegrationTestWidgetsFlutterBinding.ensureInitialized (package:integration_test/integration_test.dart:159:5)
#2 _testMain (file:///var/folders/tv/xxxx/T/flutter_tools.xxx/flutter_test_listener.xxxxx/listener.dart:21:40)
#7 Declarer.declare (package:test_api/src/backend/declarer.dart:169:7)
#8 RemoteListener.start.<anonymous closure>.<anonymous closure> (package:test_api/src/backend/remote_listener.dart:120:24)