Search code examples
flutterdartflutter-testdart-unittest

How to unit test a stream that is returned from a function?


I have the below setup:

class Resource<T> {
  T? data;
  String? error;

  Resource._({this.data, this.error});
  factory Resource.success(T? data) => Resource._(data: data);
  factory Resource.error(String error) => Resource._(error: error);
  factory Resource.loading() => Resource._(); 
}

 class CategoriesRepo{
    Stream<Resource<List<Category>>> getAllCategories() async* {
        yield Resource.loading();
        yield (Resource.error(NetworkErrors.NO_INTERNET));
      }
}

test('Loading then error',
      () async {
    final categoriesRepo = CategoriesRepo();
    expect(
        categoriesRepo.getAllCategories(),
        emitsInOrder([
          Resource<List<Category>>.loading(),
          Resource<List<Category>>.error(""),
        ]));
  });

I am getting this error:

test\unit-tests\screens\categories\categories_repo_test_test.dart       main.<fn>.<fn>

Expected: should emit an event that <Instance of 'Resource<List<Category>>'>
  Actual: <Instance of '_ControllerStream<Resource<List<Category>>>'>
   Which: emitted * Instance of 'Resource<List<Category>>'
                  * Instance of 'Resource<List<Category>>'
                  x Stream closed.

How can I properly test the above Stream?


Solution

  • The test itself is valid, however there is a small problem when it comes to assertion and comparision of objects. Basically Resource.loading() == Resource.loading() is false so the assertion fails.

    Why is it false? By default Dart objects (aside from primitives) are equal only when they are the same instance.

    To make your assertion and this kind of tests work you need to implement == operator and hashCode for your objects. You can do it manually, but that's a bit unproductive. A lot of people use package equatable or freezed, equatable is a bit easier as it doesn't involve code generation and is recommended by bloc (stream-based state management) authors.

    import 'package:equatable/equatable.dart';
    
    class Resource<T> extends Equatable {
      T? data;
      String? error;
    
      Resource._({this.data, this.error});
      factory Resource.success(T? data) => Resource._(data: data);
      factory Resource.error(String error) => Resource._(error: error);
      factory Resource.loading() => Resource._();
    
      @override
      List<Object?> get props => [data, error];
    }
    

    Of course you can also just change the assertion so it no longer uses comparision of objects, but predicates and matchers, but this isn't pretty.

    expect(
      categoriesRepo.getAllCategories(),
      emitsInOrder(
        [
          predicate<Resource<List<Category>>>(
              (r) => r.error == null && r.data == null),
          predicate<Resource<List<Category>>>(
              (r) => r.error == "" && r.data == null),
        ],
      ),
    );