如何为使用 Connectivity 插件的小部件编写 Flutter 小部件测试
How to write Flutter widget test for a widget that uses the Connectivity plugin
我有一个屏幕使用来自 Flutter 开发团队的 connectivity 包。当我不使用连接包时,我能够为这个小部件构建黄金图像,但是当我按照 Flutter 开发团队在 pub.dev 页面上概述的方式添加它时,我遇到异常 运行 通过 flutter test --update-goldens
命令进行测试。
我已经包含了测试文件(test/widget/widget_test.dart), main.dart, welcome_screen.dart,以及 运行 测试的输出。我尝试在网上寻找其他人遇到的类似问题,但我的努力没有结果;我正在寻求解决此问题的帮助。任何意见或建议将不胜感激!
输出
来自 flutter test --update-goldens
══╡ EXCEPTION CAUGHT BY SERVICES LIBRARY ╞══════════════════════════════════════════════════════════
The following MissingPluginException was thrown while activating platform stream on channel
plugins.flutter.io/connectivity_status:
MissingPluginException(No implementation found for method listen on channel
plugins.flutter.io/connectivity_status)
When the exception was thrown, this was the stack:
#0 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:156:7)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
...
════════════════════════════════════════════════════════════════════════════════════════════════════
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following MissingPluginException was thrown running a test:
MissingPluginException(No implementation found for method check on channel
plugins.flutter.io/connectivity)
When the exception was thrown, this was the stack:
#0 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:156:7)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
...
The test description was:
WelcomeScreen Golden test
════════════════════════════════════════════════════════════════════════════════════════════════════
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following message was thrown:
Multiple exceptions (2) were detected during the running of the current test, and at least one was
unexpected.
════════════════════════════════════════════════════════════════════════════════════════════════════
00:04 +14 -1: /Users/---/Documents/---/---/---/test/widget/widget_test.dart: WelcomeScreen Golden test [E]
Test failed. See exception logs above.
The test description was: WelcomeScreen Golden test
00:04 +14 -1: Some tests failed.
测试:widget_test.dart
void main() {
testWidgets('WelcomeScreen Golden test', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
await expectLater(
find.byType(MyApp),
matchesGoldenFile('main.png'),
);
});
}
main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
FocusManager.instance.primaryFocus.unfocus();
},
child: MaterialApp(
initialRoute: WelcomeScreen.id,
routes: {
WelcomeScreen.id: (context) => WelcomeScreen(),
DashboardScreen.id: (context) => DashboardScreen(),
},
),
);
}
}
屏幕:welcome_screen.dart
class WelcomeScreen extends StatefulWidget {
static const String id = 'welcome_screen';
@override
_WelcomeScreenState createState() => _WelcomeScreenState();
}
class _WelcomeScreenState extends State<WelcomeScreen> {
ConnectivityResult _connectionStatus = ConnectivityResult.none;
final Connectivity _connectivity = Connectivity();
StreamSubscription<ConnectivityResult> _connectivitySubscription;
String username = '';
String password = '';
bool isLoading = false;
@override
void initState() {
super.initState();
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
initConnectivity();
}
@override
void dispose() {
_connectivitySubscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Image.asset('images/logo.png'),
RoundedTextField(
textInputAction: TextInputAction.next,
placeholder: 'Username',
icon: Icons.person,
color: Colors.lightBlueAccent,
onChanged: (value) {
setState(() {
username = value;
});
},
),
RoundedTextField(
textInputAction: TextInputAction.done,
placeholder: 'Password',
icon: Icons.lock,
color: Colors.lightBlueAccent,
password: true,
onChanged: (value) {
setState(() {
password = value;
});
},
),
isLoading
? Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 22.0),
child: CircularProgressIndicator(),
),
)
: RoundedButton(
disabled: isLoading,
title: 'Log In',
color: Colors.lightBlueAccent,
onPressed: (_connectionStatus == ConnectivityResult.mobile || _connectionStatus == ConnectivityResult.wifi)
? () async {
setState(() {
isLoading = true;
});
try {
Login login = await API().login(username, password);
if (login.appUserKey != 0) {
Navigator.pushNamed(context, DashboardScreen.id);
}
} catch (e) {
print(e);
}
setState(() {
isLoading = false;
});
}
: null,
),
],
),
),
);
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initConnectivity() async {
ConnectivityResult result;
// Platform messages may fail, so we use a try/catch PlatformException.
try {
result = await _connectivity.checkConnectivity();
} on PlatformException catch (e) {
print(e.toString());
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) {
return Future.value(null);
}
return _updateConnectionStatus(result);
}
Future<void> _updateConnectionStatus(ConnectivityResult result) async {
switch (result) {
case ConnectivityResult.wifi:
case ConnectivityResult.mobile:
setState(() => _connectionStatus = result);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Connected to network'),
duration: Duration(seconds: 1),
),
);
break;
case ConnectivityResult.none:
setState(() => _connectionStatus = result);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Disconnected from network'),
duration: Duration(seconds: 1),
),
);
break;
default:
setState(() => _connectionStatus = null);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Connectivity failed'),
duration: Duration(seconds: 1),
),
);
break;
}
}
}
我解决了我的问题。
小部件正在尝试 运行 一种方法,该方法在进行测试的平台上不存在 运行。我决定模拟 Connectivity class,将模拟的 class 发送到小部件,然后让小部件本身检查 type of class 它已收到以确定是否应尝试 运行 listen 方法;其调用之前导致异常被抛出的方法。我使用 Mockito 包来轻松模拟服务。
我已经包含了为我解决问题的相关代码片段。
测试:widget_test.dart
重要:使用 Mockito 模拟连接 class
class MockConnectivity extends Mock implements Connectivity {}
void main() {
testWidgets('WelcomeScreen', (WidgetTester tester) async {
await tester.runAsync(() async {
await tester.pumpWidget(
MaterialApp(
home: WelcomeScreen(
connectivity: MockConnectivity(),
),
),
);
await tester.pumpAndSettle();
});
await expectLater(
find.byType(WelcomeScreen),
matchesGoldenFile('welcome_screen_portrait.png'),
);
});
}
main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: WelcomeScreen.id,
routes: {
WelcomeScreen.id: (context) => WelcomeScreen(
connectivity: Connectivity(),
),
},
);
}
}
welcome_screen.dart
class WelcomeScreen extends StatefulWidget {
static const String id = 'welcome_screen';
final Connectivity connectivity;
WelcomeScreen({this.connectivity});
@override
_WelcomeScreenState createState() => _WelcomeScreenState();
}
class _WelcomeScreenState extends State<WelcomeScreen> {
Connectivity _connectivity;
ConnectivityResult _connectionStatus = ConnectivityResult.none;
StreamSubscription<ConnectivityResult> _connectivitySubscription;
@override
void initState() {
super.initState();
_connectivity = widget.connectivity;
if (_connectivity.runtimeType == Connectivity) {
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
}
initConnectivity();
}
@override
void dispose() {
controller.dispose();
if (_connectivity.runtimeType == Connectivity) {
_connectivitySubscription.cancel();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Text('Welcome'),
);
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initConnectivity() async {
ConnectivityResult result;
// Platform messages may fail, so we use a try/catch PlatformException.
try {
result = await _connectivity.checkConnectivity();
} on PlatformException catch (e) {
print(e.toString());
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) {
return Future.value(null);
}
return _updateConnectionStatus(result);
}
Future<void> _updateConnectionStatus(ConnectivityResult result) async {
switch (result) {
case ConnectivityResult.wifi:
case ConnectivityResult.mobile:
setState(() => _connectionStatus = result);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Connected to network'),
duration: Duration(seconds: 1),
),
);
break;
case ConnectivityResult.none:
setState(() => _connectionStatus = result);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Disconnected from network'),
duration: Duration(seconds: 1),
),
);
break;
default:
setState(() => _connectionStatus = null);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Connectivity failed'),
duration: Duration(seconds: 1),
),
);
break;
}
}
}
我有一个屏幕使用来自 Flutter 开发团队的 connectivity 包。当我不使用连接包时,我能够为这个小部件构建黄金图像,但是当我按照 Flutter 开发团队在 pub.dev 页面上概述的方式添加它时,我遇到异常 运行 通过 flutter test --update-goldens
命令进行测试。
我已经包含了测试文件(test/widget/widget_test.dart), main.dart, welcome_screen.dart,以及 运行 测试的输出。我尝试在网上寻找其他人遇到的类似问题,但我的努力没有结果;我正在寻求解决此问题的帮助。任何意见或建议将不胜感激!
输出
来自 flutter test --update-goldens
══╡ EXCEPTION CAUGHT BY SERVICES LIBRARY ╞══════════════════════════════════════════════════════════
The following MissingPluginException was thrown while activating platform stream on channel
plugins.flutter.io/connectivity_status:
MissingPluginException(No implementation found for method listen on channel
plugins.flutter.io/connectivity_status)
When the exception was thrown, this was the stack:
#0 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:156:7)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
...
════════════════════════════════════════════════════════════════════════════════════════════════════
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following MissingPluginException was thrown running a test:
MissingPluginException(No implementation found for method check on channel
plugins.flutter.io/connectivity)
When the exception was thrown, this was the stack:
#0 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:156:7)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
...
The test description was:
WelcomeScreen Golden test
════════════════════════════════════════════════════════════════════════════════════════════════════
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following message was thrown:
Multiple exceptions (2) were detected during the running of the current test, and at least one was
unexpected.
════════════════════════════════════════════════════════════════════════════════════════════════════
00:04 +14 -1: /Users/---/Documents/---/---/---/test/widget/widget_test.dart: WelcomeScreen Golden test [E]
Test failed. See exception logs above.
The test description was: WelcomeScreen Golden test
00:04 +14 -1: Some tests failed.
测试:widget_test.dart
void main() {
testWidgets('WelcomeScreen Golden test', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
await expectLater(
find.byType(MyApp),
matchesGoldenFile('main.png'),
);
});
}
main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
FocusManager.instance.primaryFocus.unfocus();
},
child: MaterialApp(
initialRoute: WelcomeScreen.id,
routes: {
WelcomeScreen.id: (context) => WelcomeScreen(),
DashboardScreen.id: (context) => DashboardScreen(),
},
),
);
}
}
屏幕:welcome_screen.dart
class WelcomeScreen extends StatefulWidget {
static const String id = 'welcome_screen';
@override
_WelcomeScreenState createState() => _WelcomeScreenState();
}
class _WelcomeScreenState extends State<WelcomeScreen> {
ConnectivityResult _connectionStatus = ConnectivityResult.none;
final Connectivity _connectivity = Connectivity();
StreamSubscription<ConnectivityResult> _connectivitySubscription;
String username = '';
String password = '';
bool isLoading = false;
@override
void initState() {
super.initState();
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
initConnectivity();
}
@override
void dispose() {
_connectivitySubscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Image.asset('images/logo.png'),
RoundedTextField(
textInputAction: TextInputAction.next,
placeholder: 'Username',
icon: Icons.person,
color: Colors.lightBlueAccent,
onChanged: (value) {
setState(() {
username = value;
});
},
),
RoundedTextField(
textInputAction: TextInputAction.done,
placeholder: 'Password',
icon: Icons.lock,
color: Colors.lightBlueAccent,
password: true,
onChanged: (value) {
setState(() {
password = value;
});
},
),
isLoading
? Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 22.0),
child: CircularProgressIndicator(),
),
)
: RoundedButton(
disabled: isLoading,
title: 'Log In',
color: Colors.lightBlueAccent,
onPressed: (_connectionStatus == ConnectivityResult.mobile || _connectionStatus == ConnectivityResult.wifi)
? () async {
setState(() {
isLoading = true;
});
try {
Login login = await API().login(username, password);
if (login.appUserKey != 0) {
Navigator.pushNamed(context, DashboardScreen.id);
}
} catch (e) {
print(e);
}
setState(() {
isLoading = false;
});
}
: null,
),
],
),
),
);
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initConnectivity() async {
ConnectivityResult result;
// Platform messages may fail, so we use a try/catch PlatformException.
try {
result = await _connectivity.checkConnectivity();
} on PlatformException catch (e) {
print(e.toString());
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) {
return Future.value(null);
}
return _updateConnectionStatus(result);
}
Future<void> _updateConnectionStatus(ConnectivityResult result) async {
switch (result) {
case ConnectivityResult.wifi:
case ConnectivityResult.mobile:
setState(() => _connectionStatus = result);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Connected to network'),
duration: Duration(seconds: 1),
),
);
break;
case ConnectivityResult.none:
setState(() => _connectionStatus = result);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Disconnected from network'),
duration: Duration(seconds: 1),
),
);
break;
default:
setState(() => _connectionStatus = null);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Connectivity failed'),
duration: Duration(seconds: 1),
),
);
break;
}
}
}
我解决了我的问题。
小部件正在尝试 运行 一种方法,该方法在进行测试的平台上不存在 运行。我决定模拟 Connectivity class,将模拟的 class 发送到小部件,然后让小部件本身检查 type of class 它已收到以确定是否应尝试 运行 listen 方法;其调用之前导致异常被抛出的方法。我使用 Mockito 包来轻松模拟服务。
我已经包含了为我解决问题的相关代码片段。
测试:widget_test.dart
重要:使用 Mockito 模拟连接 class
class MockConnectivity extends Mock implements Connectivity {}
void main() {
testWidgets('WelcomeScreen', (WidgetTester tester) async {
await tester.runAsync(() async {
await tester.pumpWidget(
MaterialApp(
home: WelcomeScreen(
connectivity: MockConnectivity(),
),
),
);
await tester.pumpAndSettle();
});
await expectLater(
find.byType(WelcomeScreen),
matchesGoldenFile('welcome_screen_portrait.png'),
);
});
}
main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: WelcomeScreen.id,
routes: {
WelcomeScreen.id: (context) => WelcomeScreen(
connectivity: Connectivity(),
),
},
);
}
}
welcome_screen.dart
class WelcomeScreen extends StatefulWidget {
static const String id = 'welcome_screen';
final Connectivity connectivity;
WelcomeScreen({this.connectivity});
@override
_WelcomeScreenState createState() => _WelcomeScreenState();
}
class _WelcomeScreenState extends State<WelcomeScreen> {
Connectivity _connectivity;
ConnectivityResult _connectionStatus = ConnectivityResult.none;
StreamSubscription<ConnectivityResult> _connectivitySubscription;
@override
void initState() {
super.initState();
_connectivity = widget.connectivity;
if (_connectivity.runtimeType == Connectivity) {
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
}
initConnectivity();
}
@override
void dispose() {
controller.dispose();
if (_connectivity.runtimeType == Connectivity) {
_connectivitySubscription.cancel();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Text('Welcome'),
);
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initConnectivity() async {
ConnectivityResult result;
// Platform messages may fail, so we use a try/catch PlatformException.
try {
result = await _connectivity.checkConnectivity();
} on PlatformException catch (e) {
print(e.toString());
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) {
return Future.value(null);
}
return _updateConnectionStatus(result);
}
Future<void> _updateConnectionStatus(ConnectivityResult result) async {
switch (result) {
case ConnectivityResult.wifi:
case ConnectivityResult.mobile:
setState(() => _connectionStatus = result);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Connected to network'),
duration: Duration(seconds: 1),
),
);
break;
case ConnectivityResult.none:
setState(() => _connectionStatus = result);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Disconnected from network'),
duration: Duration(seconds: 1),
),
);
break;
default:
setState(() => _connectionStatus = null);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Connectivity failed'),
duration: Duration(seconds: 1),
),
);
break;
}
}
}