Search code examples
flutterunit-testingclean-architecture

Unexpected null return when Mocking a Repository return. Flutter


I just started with TDD and Clean Architecture in Flutter following a series at https://resocoder.com/2019/08/29/flutter-tdd-clean-architecture-course-2-entities-use-cases/.

At this stage there is no implementation of the Repository yet, just the abstract class which gets mocked for testing purposes and a use case. Now if I got this reverse dependency concept right, the repository gets injected into the use case class and the use case method will call correct repository method and return its result as in

abstract class NumberTriviaRepository {
  // here we define the contracts, the methods that the implementation of this class
  // (in the repository of the data layer ) must use.

  Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number);
  Future<Either<Failure, NumberTrivia>> getRandomNumberTriva();

}
class GetConcreteNumberTrivia {
  final NumberTriviaRepository repository;
  const GetConcreteNumberTrivia({required this.repository}) : super();

  Future<Either<Failure, NumberTrivia>> execute({required int number}) async {
     return await repository.getConcreteNumberTrivia(number);
  }
}

Then in the test Mockito is used:

  1. A mock class which implements the repository class gets created and used in the test
  2. The use case gets instantiated
  3. An instance of the Entity gets created and used as the mocked return of the mocked repository method using when and thenAnswer.
  4. the actual use case gets called
  5. mocked Entity and use case result get compared.
// create a mock repository class that implements the repository for the test
class MockNumberTriviaRepository extends Mock
    implements NumberTriviaRepository {}

void main() {
  // define a variable for the mock repository class
  late MockNumberTriviaRepository mockNumberTriviaRepository;
  // define a variable for the use case with the mock repository class
  late GetConcreteNumberTrivia usecase;

  setUp(() {
    // initialise the variable instantiating the mock repository class
    mockNumberTriviaRepository = MockNumberTriviaRepository();
    // initialise the variable instantiating the use case injecting the mock repository class
    usecase = GetConcreteNumberTrivia(repository: mockNumberTriviaRepository);
  });

  // test value
  const tNumber = 1;
  // mocked Entity
  const tNumberTrivia = NumberTrivia(text: 'test', number: tNumber);


  test('should get trivia for the number form the repository', () async {
    // arrange

    when(
            // call the repository method to be tested from the mocked repository
            mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber))

        // force the mocked repository class method to return a specific value,
        // in this case the Right side of the method's Either return type with the mocked Entity
        .thenAnswer((_) async => const Right(tNumberTrivia));
    // act

    // get an Entity from the use case 
    final result = await usecase.execute(number: tNumber);

    // assert

    // compare the two Entities
    expect(result, const Right(tNumberTrivia));
    // check that repository method has been called with the test value
    verify(mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber));
    // chech that no other repository methods were called
    verifyNoMoreInteractions(mockNumberTriviaRepository);
  });
}

When I run the test I get the error message:

type 'Null' is not a subtype of type 'Future<Either<Failure, NumberTrivia>>'
test/features/number_trivia/domain/usecases/get_concrete_number_trivia_test.dart 11:7   MockNumberTriviaRepository.getConcreteNumberTrivia
test/features/number_trivia/domain/usecases/get_concrete_number_trivia_test.dart 38:40  main.<fn>

I tried commenting out the assert part of the test and the call of the use case, but the I still get the error, so I guess it comes from the "forced return of the mocked repository in .thenAnswer((_) async => const Right(tNumberTrivia)). Any Idea why the mock Entity is not returned?

Many thanks


Solution

  • Ok.. found the problem.. I rushed and I haven't really thought that the series I'm following is a bit outdated and meanwhile we got null-safety, which scrambled a few eggs.. so as stated in https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md the method for mocking classes is changed..

    Old way

    class MockNumberTriviaRepository extends Mock
        implements NumberTriviaRepository {}
    

    New way (I went with the automatic way, but there is also a manual way)

    1. Install build_runner as dev dependency.
    2. Add mockito's annotations import and mark the repository class to be mocked.
    import 'package:mockito/annotations.dart';
    @GenerateMocks([NumberTriviaRepository])
    
    1. Run dart run build_runner build which will generate a <usecase file name>.mock.dart file in the same location of the usecase file which will contain the MockRepository class to be used in the test (MockNumberTriviaRepository I my case).

    2. Import that file

    import 'get_concrete_number_trivia_test.mocks.dart';
    

    Now the test can use the proper Mocked repository class.

    Still is unclear to me why it's creating a <usecase file name>.mock.dart file in the same location of the usecase file, and not a <repository file name>.mock.dart file in the same location of the repository file. After all is repository mocked class that it's generating.

    This leaves me wandering.. what should be done with the other tests for rest of the use cases for that repository? I guess just import the same <usecase file name>.mock.dart file.

    Solution 2 just switch from Mockito to Mocktail which does work as the old Mockito without any code generation.. Thanx Felix Angel once again.

    You'll just need to add () => to both stubbing and verifying calls.

    from

    when(mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber))
    
    verify(mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber));
    

    to

    when(() => mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber))
    
    verify(() => mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber));