使用flutter_bloc颤动testWidgets - 测试仅在一起执行时失败

问题描述 投票:0回答:2

我在颤动中附加小部件测试时遇到问题。当我单独运行测试时,每个测试都成功;但是,当我运行整个main()方法时,前三个测试成功,但最后两个测试失败,出现以下异常:

Expected: exactly one matching node in the widget tree
  Actual: ?:<zero widgets with type "SuccessDialog" (ignoring offstage widgets)>

我理解异常意味着我期待的小部件不存在 - 我不明白为什么测试在单独运行时成功但在其他测试后运行后失败。是否有一些实例我需要在每次测试后“重置”?

我试过插入“final 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,
                    ),
                  );
                }
              },
            ),
          ],
        )
      ],
    );
  }
}
flutter flutter-test
2个回答
0
投票

这可能是因为您的测试改变了一些全局变量但不重置它们的值。

使其安全的一种方法是始终使用setUptearDown而不是直接改变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);     
});

这确保即使测试失败也始终重置该值 - 以便其他测试功能正常。


0
投票

发现了问题。我在创建一个bloc实例时使用了单例模式 - 这导致状态持续跨越不同的widget对象。不太可能有人会遇到我遇到的同样的问题,但下面是我改变以缓解问题的代码

旧有问题的代码:

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();

  //...
}
© www.soinside.com 2019 - 2024. All rights reserved.