我在附带的窗口小部件测试中遇到了问题。当我分别运行测试时,每个测试都会成功;但是,当我运行整个main()方法时,前三个测试成功,但后两个失败,但有以下异常:
Expected: exactly one matching node in the widget tree
Actual: ?:<zero widgets with type "SuccessDialog" (ignoring offstage widgets)>
我知道该异常意味着我所期望的小部件不存在-我不明白的是为什么当单独运行时测试成功但在其他测试后运行却失败了。每次测试后是否都需要“重置”某些实例?
我尝试插入“最终SemanticsHandle handle = tester.ensureSemantics();”在每个测试和“ handle.dispose();”开始时在每次测试结束时,却得到了相同的结果。
编辑: 经过进一步调查后,看来问题出在我如何使用flutter_bloc软件包管理bloc实例。我已经更改了测试以为每个测试创建一个新的testWidget实例,但是仍然遇到相同的问题。我可能会缺少什么导致bloc实例在testWidget对象之间持久存在?
我的新测试代码如下:
main() {
MvnoMockClient.init();
testWidgets(
'Voucher Redemption: Tapping redeem when no values were entered yields 2 field errors',
(WidgetTester tester) async {
Widget testWidget = MediaQuery(
data: MediaQueryData(),
child: MaterialApp(
home: VoucherRedemptionPage(onSuccess: () {}, onFail: () {}),
),
);
await tester.pumpWidget(testWidget);
await tester.tap(find.byType(PrimaryCardButton));
await tester.pump();
expect(find.text("Field is required"), findsNWidgets(2));
});
testWidgets(
'Voucher Redemption: Tapping redeem when only voucher number was entered yields one field error',
(WidgetTester tester) async {
Widget testWidget = MediaQuery(
data: MediaQueryData(),
child: MaterialApp(
home: VoucherRedemptionPage(onSuccess: () {}, onFail: () {}),
),
);
await tester.pumpWidget(testWidget);
await tester.enterText(find.byType(PlainTextField), "0000000000");
await tester.tap(find.byType(PrimaryCardButton));
await tester.pump();
expect(find.text("Field is required"), findsOneWidget);
});
testWidgets(
'Voucher Redemption: Tapping redeem when only mobile number was entered yields one field error',
(WidgetTester tester) async {
Widget testWidget = MediaQuery(
data: MediaQueryData(),
child: MaterialApp(
home: VoucherRedemptionPage(onSuccess: () {}, onFail: () {}),
),
);
await tester.pumpWidget(testWidget);
await tester.enterText(find.byType(MsisdnField), "0815029249");
await tester.tap(find.byType(PrimaryCardButton));
await tester.pump();
expect(find.text("Field is required"), findsOneWidget);
});
testWidgets(
'Voucher Redemption: A successful server response yields a success dialog',
(WidgetTester tester) async {
Widget testWidget = MediaQuery(
data: MediaQueryData(),
child: MaterialApp(
home: VoucherRedemptionPage(onSuccess: () {}, onFail: () {}),
),
);
await tester.pumpWidget(testWidget);
await tester.enterText(find.byType(PlainTextField), "0000000000");
await tester.enterText(find.byType(MsisdnField), "0815029249");
await tester.tap(find.text("REDEEM"));
await tester.pump();
expect(find.byType(SuccessDialog), findsOneWidget);
});
testWidgets(
'Voucher Redemption: An unsuccessful server response yields an error dialog',
(WidgetTester tester) async {
Widget testWidget = MediaQuery(
data: MediaQueryData(),
child: MaterialApp(
home: VoucherRedemptionPage(onSuccess: () {}, onFail: () {}),
),
);
await tester.pumpWidget(testWidget);
gToken = "invalid";
await tester.enterText(find.byType(PlainTextField), "0000000000");
await tester.enterText(find.byType(MsisdnField), "0815029249");
await tester.tap(find.byType(PrimaryCardButton));
await tester.pump();
gToken = "validToken";
expect(find.byType(ErrorDialog), findsOneWidget);
});
}
作为其他参考,我还包括以下VoucherRedemptionPage和VoucherRedemptionScreen的代码:
class VoucherRedemptionPage extends StatelessWidget {
final onSuccess;
final onFail;
const VoucherRedemptionPage({Key key, @required this.onSuccess, @required this.onFail})
: super(key: key);
@override
Widget build(BuildContext context) {
var _voucherRedemptionBloc = new VoucherRedemptionBloc();
return Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/" + gFlavor + "/primary_background.png"),
fit: BoxFit.cover),
),
child: new Scaffold(
backgroundColor: Colors.transparent,
appBar: new AppBar(
title: new Text(gDictionary.find("Redeem Voucher")),
),
body: new VoucherRedemptionScreen(
voucherRedemptionBloc: _voucherRedemptionBloc,
onSuccess: this.onSuccess,
onFail: this.onFail,
),
),
);
}
}
class VoucherRedemptionScreen extends StatefulWidget {
const VoucherRedemptionScreen({
Key key,
@required VoucherRedemptionBloc voucherRedemptionBloc,
@required this.onSuccess,
@required this.onFail,
}) : _voucherRedemptionBloc = voucherRedemptionBloc,
super(key: key);
final VoucherRedemptionBloc _voucherRedemptionBloc;
final onSuccess;
final onFail;
@override
VoucherRedemptionScreenState createState() {
return new VoucherRedemptionScreenState(
_voucherRedemptionBloc, onSuccess, onFail);
}
}
class VoucherRedemptionScreenState extends State<VoucherRedemptionScreen> {
final VoucherRedemptionBloc _voucherRedemptionBloc;
final onSuccess;
final onFail;
TextEditingController _msisdnController = TextEditingController();
TextEditingController _voucherPinController = TextEditingController();
GlobalKey<FormState> _formKey = GlobalKey<FormState>();
VoucherRedemptionScreenState(
this._voucherRedemptionBloc, this.onSuccess, this.onFail);
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<VoucherRedemptionEvent, VoucherRedemptionState>(
bloc: _voucherRedemptionBloc,
builder: (
BuildContext context,
VoucherRedemptionState currentState,
) {
if (currentState is VoucherRedemptionInitial) {
_voucherPinController.text = currentState.scannedNumber;
return _buildFormCard();
}
if (currentState is VoucherRedemptionLoading) {
return Center(
child: CircularProgressIndicator(),
);
}
if (currentState is VoucherRedemptionSuccess) {
return SuccessDialog(
title: gDictionary.find("Voucher Redeemed Successfully"),
description: currentState.successMessage,
closeText: gDictionary.find("OK"),
closeAction: () {
this.onSuccess();
_voucherRedemptionBloc.dispatch(ResetVoucherRedemptionState());
},
);
}
if (currentState is VoucherRedemptionError) {
return ErrorDialog(
errorCode: currentState.errorCode,
errorMessage: currentState.errorMessage,
closeText: gDictionary.find("OK"),
closeAction: () {
this.onFail();
_voucherRedemptionBloc.dispatch(ResetVoucherRedemptionState());
},
);
}
},
);
}
Widget _buildFormCard() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8), topRight: Radius.circular(8))),
padding: EdgeInsets.fromLTRB(12, 12, 12, 0),
width: double.infinity,
height: double.infinity,
child: _buildCardContent(),
);
}
Widget _buildCardContent() {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
gDictionary.find("Transaction Amount"),
style: TextStyle(
fontSize: 14,
color: Theme.of(context).primaryColorDark,
fontWeight: FontWeight.bold),
),
Container(height: 16),
Form(
key: _formKey,
child: _buildFormContent(),
),
],
),
);
}
Column _buildFormContent() {
return Column(
children: <Widget>[
PlainTextField(
controller: _voucherPinController,
label: gDictionary.find("Voucher Number"),
required: true,
),
Container(height: 16),
MsisdnField(
controller: _msisdnController,
label: gDictionary.find("Mobile Number"),
required: true,
),
Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
SecondaryCardButton(
text: gDictionary.find("SCAN VOUCHER"),
onPressed: () {
_voucherRedemptionBloc.dispatch(
ScanBarcode(),
);
},
),
Container(
width: 8.0,
),
PrimaryCardButton(
text: gDictionary.find("REDEEM"),
onPressed: () {
if (_formKey.currentState.validate()) {
_voucherRedemptionBloc.dispatch(
RedeemVoucher(
_voucherPinController.text,
_msisdnController.text,
),
);
}
},
),
],
)
],
);
}
}
答案 0 :(得分:0)
之所以会发生这种情况,是因为您的测试突变了一些全局变量,但没有重置其值。
确保安全的一种方法是始终使用setUp
和tearDown
而不是直接在main
范围内对变量进行突变:
int global = 0;
void main() {
final initialGlobalValue = global;
setUp(() {
global = 42;
});
tearDown(() {
global = initialGlobalValue;
});
test('do something with "global"', () {
expect(++global, 43);
});
test('do something with "global"', () {
// would fail without setUp/tearDown
expect(++global, 43);
});
}
类似地,如果测试需要修改变量,请使用addTearDown
而不是稍后在测试中手动重置值。
不要:
int global = 0;
test("don't", () {
global = 43;
expect(global, 43);
global = 0;
})
要做:
int global = 0;
test('do', () {
global = 43;
addTearDown(() => global = 0);
expect(global, 43);
});
这可确保即使测试失败也将始终重置该值,以便其他测试正常运行。
答案 1 :(得分:0)
发现了问题。创建整体实例时,我使用的是单例模式-这导致状态在不同的窗口小部件对象之间持续存在。任何人都不可能遇到与我相同的问题,但是下面是为缓解该问题而更改的代码
有问题的旧代码:
class VoucherRedemptionBloc
extends Bloc<VoucherRedemptionEvent, VoucherRedemptionState> {
static final VoucherRedemptionBloc _voucherRedemptionBlocSingleton =
new VoucherRedemptionBloc._internal();
factory VoucherRedemptionBloc() {
return _voucherRedemptionBlocSingleton;
}
VoucherRedemptionBloc._internal();
//...
}
更新后的工作代码:
class VoucherRedemptionBloc
extends Bloc<VoucherRedemptionEvent, VoucherRedemptionState> {
VoucherRedemptionBloc();
//...
}