felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.72k stars 3.38k forks source link

Question: How to test state class changes? #3157

Closed eRuaro closed 2 years ago

eRuaro commented 2 years ago

Description

How do I test state changes when the cubit has an initialization function?

I have a cubit which looks like this:

class IntroscreenCubit extends Cubit<IntroscreenState> {
  IntroscreenCubit() : super(IntroScreenInitial()) {
    _checkIntroScreen();
  }

  Future<void> _checkIntroScreen() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    bool _firstTime = prefs.getBool('first_time') ?? true;

    if (_firstTime) {
      prefs.setBool('first_time', false);
      emit(OnboardingScreenIntro());
    } else {
      emit(ProfileScreenIntro());
    }
  }
}

Wherein the state classes looks like this:

part of 'intro_screen_cubit.dart';

abstract class IntroscreenState extends Equatable {
  const IntroscreenState();

  @override
  List<Object> get props => [];
}

class IntroScreenInitial extends IntroscreenState {
  @override
  List<Object> get props => [];
}

class OnboardingScreenIntro extends IntroscreenState {
  @override
  List<Object> get props => [];
}

class ProfileScreenIntro extends IntroscreenState {
  @override
  List<Object> get props => [];
}

I wanted to test what the cubit would emit by following this article: https://medium.com/flutter-community/unit-testing-with-bloc-b94de9655d86

The test file looks like this:

void main() {
  group("first time opening app", () {
    late IntroscreenCubit introscreenCubit;

    setUp(() {
      // no initial values
      SharedPreferences.setMockInitialValues({});
      introscreenCubit = IntroscreenCubit();
    });

    tearDown(() {
      introscreenCubit.close();
    });

    test('initial state is IntroScreenInitial', () {
      expect(introscreenCubit.state, IntroScreenInitial());
    });

    blocTest(
      "should emit OnboardingScreenIntro()",
      build: () => introscreenCubit,
      wait: const Duration(milliseconds: 20),
      // I think this is based on the props, expecting [OnboardingScreenIntro] returns an error
      expect: () => [],
    );
  });

  group("not first time opening app", () {
    late IntroscreenCubit introscreenCubit;
    late SharedPreferences pref;
    setUp(() async {
      // no initial values
      SharedPreferences.setMockInitialValues({
        "first_time": false,
      });
      pref = await SharedPreferences.getInstance();
      introscreenCubit = IntroscreenCubit();
    });

    tearDown(() {
      introscreenCubit.close();
    });

    test('initial state is IntroScreenInitial', () {
      expect(introscreenCubit.state, IntroScreenInitial());
    });

    test('pref must be false', () {
      expect(pref.getBool('first_time'), false);
    });

    test('emitted state is [IntroScreenInitial, ProfileScreenIntro]', () {
      expectLater(
        introscreenCubit,
        emitsInOrder([
          IntroScreenInitial(),
          ProfileScreenIntro(),
          emitsDone
        ]),
      );

      introscreenCubit.close();
    });
  });
}

The last test in the first group confuses me, as I thought I'd have to emit the state class. So I tried out in the second group of what was used in the articled I linked above which uses expectLater which returns this error:

Expected: should do the following in order:
          • emit an event that IntroScreenInitial:<IntroScreenInitial()>
          • emit an event that ProfileScreenIntro:<ProfileScreenIntro()>
          • be done
  Actual: <Instance of 'IntroscreenCubit'>
   Which: was not a Stream or a StreamQueue

How do I fix the last test for the second group? Also is there a way to use blocTest instead for state class changes instead of using expectLater and test?

alestiago commented 2 years ago

Hi @eRuaro ! Thanks for submitting an issue.

The official testing documentation may help with this. There are different examples out there that can help. Let me know if the documentation is of any help 😃.

felangel commented 2 years ago

Closing for now since it doesn't appear there are any actionable next steps. Feel free to comment with any additional questions and I'm happy to continue the conversation 👍