我如何在 Flutter 中模拟一个 bloc,并发出状态以响应来自被测小部件的事件

How do I mock a bloc in Flutter, with states being emitted in response to events from a widget under test

我正在尝试测试一个使用 bloc 的小部件。我希望能够从我的模拟集团发出状态,以响应被测小部件触发的事件。我尝试了很多方法都没有成功。我不确定我是否犯了一些简单的错误,或者我是否完全错误地解决了问题。

这是一个演示我的问题的简化项目。 (完整的代码可以在 https://github.com/andrewdixon1000/flutter_bloc_mocking_issue.git 找到)

非常简单的集团

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';

class MyBloc extends Bloc<MyEvent, MyState> {
  MyBloc() : super(FirstState());

  @override
  Stream<MyState> mapEventToState(
    MyEvent event,
  ) async* {
    if (event is TriggerStateChange) {
      yield SecondState();
    }
  }
}

@immutable
abstract class MyEvent {}

class TriggerStateChange extends MyEvent {}


@immutable
abstract class MyState {}

class FirstState extends MyState {}

class SecondState extends MyState {}

我正在测试的小部件

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import 'bloc/my_bloc.dart';
import 'injection_container.dart';

class FirstPage extends StatefulWidget {
  const FirstPage({Key? key}) : super(key: key);

  @override
  _FirsPageState createState() => _FirsPageState();
}

class _FirsPageState extends State<FirstPage> {

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => serviceLocator<MyBloc>(),
      child: Scaffold(
        appBar: AppBar(title: Text("Page 1")),
        body: Container(
          child: BlocConsumer<MyBloc, MyState>(
            listener: (context, state) {
              if (state is SecondState) {
                Navigator.pushNamed(context, "SECONDPAGE");
              }
            },
            builder: (context, state) {
              if (state is FirstState) {
                return Column(
                  children: [
                    Text("State is FirstState"),
                    ElevatedButton(
                        onPressed: () {
                          BlocProvider.of<MyBloc>(context).add(TriggerStateChange());
                        },
                        child: Text("Change state")),
                  ],
                );
              } else {
                return Text("some other state");
              }
            },
          ),
        ),
      ),
    );
  }
}

我的插件测试 这就是我挣扎的地方。我正在做的是加载小部件,然后点击按钮。这会导致小部件向集团添加一个事件。我想要做的是让我的模拟集团发出一个状态来响应这个,这样小部件的 BlocConsumer 的侦听器将看到状态改变导航。正如您从代码中的注释中看到的那样,我已经尝试了一些没有运气的事情。目前,我尝试过的任何操作都会导致侦听器看到状态变化。

import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart' as mocktail;
import 'package:get_it/get_it.dart';
import 'package:test_bloc_issue/bloc/my_bloc.dart';
import 'package:test_bloc_issue/first_page.dart';

class MockMyBloc extends MockBloc<MyEvent, MyState> implements MyBloc {}
class FakeMyState extends Fake implements MyState {}
class FakeMyEvent extends Fake implements MyEvent {}

void main() {
  MockMyBloc mockMyBloc;
  mocktail.registerFallbackValue<MyState>(FakeMyState());
  mocktail.registerFallbackValue<MyEvent>(FakeMyEvent());
  mockMyBloc = MockMyBloc();

  var nextScreenPlaceHolder = Container();

  setUpAll(() async {
    final di = GetIt.instance;
    di.registerFactory<MyBloc>(() => mockMyBloc);
  });

  _loadScreen(WidgetTester tester) async {
    mocktail.when(() => mockMyBloc.state).thenReturn(FirstState());
    await tester.pumpWidget(
      MaterialApp(
        home: FirstPage(),
        routes: <String, WidgetBuilder> {
          'SECONDPAGE': (context) => nextScreenPlaceHolder
        }
      )
    );
  }

  testWidgets('test', (WidgetTester tester) async {
    await _loadScreen(tester);
    await tester.tap(find.byType(ElevatedButton));
    await tester.pump();
    
    // What do I need to do here to mock the state change that would
    // happen in the real bloc when a TriggerStateChange event is received,
    // such that the listener in my BlocConsumer will see it?
    // if tried:
    // whenListen(mockMyBloc, Stream<MyState>.fromIterable([SecondState()]));
    // and
    // mocktail.when(() => mockMyBloc.state).thenReturn(SecondState());
    await tester.pumpAndSettle();

    expect(find.byWidget(nextScreenPlaceHolder), findsOneWidget);
  });
}

我看了一下 opened a pull request with my suggestions. I highly recommend thinking of your tests in terms of notifications and reactions. In this case, I recommend having one test to verify that when the button is tapped, the correct event is added to the bloc (the bloc is notified). Then I recommend having a separate test to ensure that when the state changes from FirstState to SecondState that the correct page is rendered (the UI reacts to state changes appropriately). In the future, I highly recommend taking a look at the example apps 因为大部分都经过了全面测试。