Bloc: UnitTesting Bloc

Created on 25 Feb 2019  路  14Comments  路  Source: felangel/bloc

Hi Felix,

i am trying to write some unit tests for my pretty simple Bloc. In detail: I want to test, that a certain event should not let the bloc emit a new state or keep the same state as before.

Here is the Bloc:

class TimeTrackingBloc extends Bloc<TimeTrackingEvent, TimeTrackingState>{
  @override
  TimeTrackingState get initialState => TimeTrackingDisabled();

  @override
  Stream<TimeTrackingState> mapEventToState(TimeTrackingState currentState, TimeTrackingEvent event) async* {
    if(event is AssignmentChanged){
      print("Assignment changed");
      yield TimeTrackingIsStopped(assignment: event.assignment);
    }
    else if(event is StartTimetracking && !(currentState is TimeTrackingDisabled)){
      print("Start");
      yield TimeTrackingIsRunning(assignment: event.assignment);
    }
    else if(event is StopTimetracking && !(currentState is TimeTrackingDisabled)){
      print("Stop");
      yield TimeTrackingIsStopped(assignment: event.assignment);
    }
  }
}

Here is the test:

  test("Event StartTimeTracking does not transform State if current state is TimeTrackingDisabled", () async {
    var timeTrackingBloc = TimeTrackingBloc();

    timeTrackingBloc.dispatch(StartTimetracking());

    expect(timeTrackingBloc.state, emitsInOrder([
      TimeTrackingDisabled(),
      emitsDone
    ]));
  });

The test runs into the 30s execution timeout. I tried several other ideas of testing this case but i never got it to work correctly.

Am i missing something? Any hint would be great. Thanks in advance.

question

Most helpful comment

Yay, your implementation works!

With this in mind, i can now do what i wanted in the first place:

    var timeTrackingBloc = TimeTrackingBloc();

    expectLater(timeTrackingBloc.state, emitsInOrder([
      TimeTrackingDisabled(),
      TimeTrackingIsStopped(),
      TimeTrackingIsRunning(),
      emitsDone
    ]));

    timeTrackingBloc.dispatch(AssignmentChanged());
    timeTrackingBloc.dispatch(StartTimetracking());

    await new Future(() { timeTrackingBloc.dispose(); });

All 14 comments

Hi @SBortz thanks for opening an issue!

I believe you just need to use expectLater instead of expect.

Your test should look like:

test("Event StartTimeTracking does not transform State if current state is TimeTrackingDisabled", () async {
    var timeTrackingBloc = TimeTrackingBloc();

    expectLater(timeTrackingBloc.state, emitsInOrder([
      TimeTrackingDisabled(),
      emitsDone
    ]));

    timeTrackingBloc.dispatch(StartTimetracking());
});

In case you haven't already checked it out, the bloc documentation has some unit testing examples.

Let me know if that helps! 馃憤

That seems to have no effect at all :(

The test still runs 30s and then fails.

Have you tried removing the emitsDone?

test("Event StartTimeTracking does not transform State if current state is TimeTrackingDisabled", () async {
    var timeTrackingBloc = TimeTrackingBloc();

    expectLater(timeTrackingBloc.state, emitsInOrder([
      TimeTrackingDisabled(),
    ]));

    timeTrackingBloc.dispatch(StartTimetracking());
});

Is this the correct spelling:

expectLater(timeTrackingBloc.state, emitsInOrder([
      TimeTrackingDisabled,
      emitsDone
    ]));

or this?

expectLater(timeTrackingBloc.state, emitsInOrder([
      TimeTrackingDisabled(),
      emitsDone
    ]));

The first option finishes faster but with an error:

Expected: should do the following in order:
          * emit an event that Type:<TimeTrackingDisabled>
          * be done
  Actual: <Instance of 'BehaviorSubject<TimeTrackingState>'>
   Which: emitted * Instance of 'TimeTrackingDisabled'
            which didn't emit an event that Type:<TimeTrackingDisabled>

Ok, removing emitsDone works. But would it then still be a correct test?

What if the implemenation emits more than what the tests checks?

You can dispatch multiple events and make sure there are no events between dispatches if that makes sense. The reason emitsDone won't work is because the Bloc's state stream remains open after dispatching an event.

Is it okay to close this now? @SBortz, do you have any remaining questions around the unit tests?

Ah, i understand. Then i kick out emitsDone out of my tests.

Is it somehow possible to sum up all emitted events into a list and dispose the bloc then?
I think it would be cool to have some more examples in the docs that cover questions like this.

@SBortz yeah you can call dispose on the bloc at the end if you'd like and it will close the streams.
I'll try to add some more examples around your use-case later today 馃憤

Thanks a lot for your quick reaction!

This works now for me:

  test("Event StartTimeTracking does not transform State if current state is TimeTrackingDisabled", () async {
    var timeTrackingBloc = TimeTrackingBloc();

    timeTrackingBloc.dispatch(StartTimetracking());
    timeTrackingBloc.dispose();

    var actualStates = await timeTrackingBloc.state.toList();

    expect(actualStates.first is TimeTrackingDisabled, true);
    expect(actualStates.length, 1);
  });

No problem, and good approach! I'll add something similar to the docs later today 馃憤

Sorry, that i still comment on this issue.
It doesn麓t matter what i try to do, i am not able to test the correct order of emitted States:

  test("AssignmentChanged transforms to TimeTrackingIsStopped-2", () async {
    var timeTrackingBloc = TimeTrackingBloc();

    timeTrackingBloc.dispatch(AssignmentChanged());
    timeTrackingBloc.dispatch(StartTimetracking());

    expect(timeTrackingBloc.state, emitsInOrder([
      TimeTrackingDisabled,
      TimeTrackingIsStopped,
      TimeTrackingIsRunning
    ]));
  });

Output:

Assignment changed
Start

Expected: should do the following in order:
          * emit an event that Type:<TimeTrackingDisabled>
          * emit an event that Type:<TimeTrackingIsStopped>
          * emit an event that Type:<TimeTrackingIsRunning>
  Actual: <Instance of 'BehaviorSubject<TimeTrackingState>'>
   Which: emitted * Instance of 'TimeTrackingDisabled'
                  * Instance of 'TimeTrackingIsStopped'
                  * Instance of 'TimeTrackingIsRunning'
            which didn't emit an event that Type:<TimeTrackingDisabled>

This is the dispose() approach:

  test("AssignmentChanged transforms to TimeTrackingIsStopped", () async {
    var timeTrackingBloc = TimeTrackingBloc();

    timeTrackingBloc.dispatch(AssignmentChanged());

    await Future.delayed(Duration(milliseconds: 1));
    timeTrackingBloc.dispose();

    var actualStates = await timeTrackingBloc.state.toList();

    print(actualStates);
    expect(actualStates.length, 2);
    expect(actualStates.first is TimeTrackingDisabled, true);
    expect(actualStates[1] is TimeTrackingIsStopped, true);
  });

Output:

Assignment changed
Start
package:test_api                      expect
test\booking_bar_bloc_test.dart 40:5  main.<fn>
===== asynchronous gap ===========================
dart:async                            _AsyncAwaitCompleter.completeError
test\booking_bar_bloc_test.dart       main.<fn>
===== asynchronous gap ===========================
dart:async                            _asyncThenWrapperHelper
test\booking_bar_bloc_test.dart       main.<fn>

Expected: <3>
  Actual: <1>

I am happy for any further help with this.

I think you鈥檙e just providing the type instead of an instance and you should use expectLater instead of expect.

var timeTrackingBloc = TimeTrackingBloc();

expectLater(timeTrackingBloc.state, emitsInOrder([
  TimeTrackingDisabled(),
  TimeTrackingIsStopped(),
  TimeTrackingIsRunning(),
]));

timeTrackingBloc.dispatch(AssignmentChanged());
timeTrackingBloc.dispatch(StartTimeTracking());

In addition, if you want to be able to compare instances of state you need to override hashCode and == operator or use equatable

If you鈥檙e able to give me access to the repo I can create a PR to fix the tests.

Yay, your implementation works!

With this in mind, i can now do what i wanted in the first place:

    var timeTrackingBloc = TimeTrackingBloc();

    expectLater(timeTrackingBloc.state, emitsInOrder([
      TimeTrackingDisabled(),
      TimeTrackingIsStopped(),
      TimeTrackingIsRunning(),
      emitsDone
    ]));

    timeTrackingBloc.dispatch(AssignmentChanged());
    timeTrackingBloc.dispatch(StartTimetracking());

    await new Future(() { timeTrackingBloc.dispose(); });
Was this page helpful?
0 / 5 - 0 ratings

Related issues

frankrod picture frankrod  路  3Comments

shawnchan2014 picture shawnchan2014  路  3Comments

timtraversy picture timtraversy  路  3Comments

tigranhov picture tigranhov  路  3Comments

rsnider19 picture rsnider19  路  3Comments