unit-testingflutterflutter-moor

flutter: unit testing dao in moor


I've started unit testing the first time. I'm following tutorials of resoCoder

here is my test code where I'm mocking my dbManager class but I could not mock DAO's as they are auto-generated in moor and there is no setter method for them.

class MockDbManager extends Mock implements DbManager{}

void main() {
  RecipeLocalDataSource dataSource;
  MockDbManager _dbManager;
  setUp(() {
    _dbManager = MockDbManager();
    dataSource = RecipeLocalDataSource(_dbManager);
  });

  group('Search Food Table', (){
    List<FoodTableData> getFoodTable(){
      var list = [];
      for(var i =1; i <=5 ; i++){
        list.add(FoodTableData(id: i, name: 'item $i'));
      }
      return list;
    }
    var searchQuery = 'query';

    test('Should return foodTableData when query is successful', (){
      //arrange
      when(_dbManager.foodTableDao.searchFoods(searchQuery)).thenAnswer((realInvocation) async => getFoodTable());
      //act
      var result = dataSource.searchFoodTable('test');
      //assert
      verify(_dbManager.foodTableDao.searchFoods(searchQuery));
      expect(getFoodTable(), result);
    });
  });
}

I'm getting the following error

NoSuchMethodError: The method 'searchFoods' was called on null.
Receiver: null
Tried calling: searchFoods("query")

I understood the error but don't know how to solve that.

Also, I'm getting a similar issue with preferenceManager class as well, where I'm having a getter for UserPrefs.

UserPrefs get user => UserPrefs(_pref);

when I'm accessing _prefManager.user.name for the test, it throws the same error. How can I tackle that as well?


Solution

  • I believe you're missing a level of mocking, leading to the null exception. Keep in mind the mocked class returns null for everything. You have to provide values for everything.

    You've mocked DbManager, but not the foodTableDao field inside DbManager.

    // I don't have your classes, so these are just my guesses at matching their interface
    abstract class TableDao {
      String searchFoods();
    }
    
    abstract class DbManager {
      TableDao foodTableDao;
    }
    
    
    class MockDbManager extends Mock implements DbManager {}
    
    class MockTableDao extends Mock implements TableDao {}
    // ↑ Define this mocked class as well
    
    void main() {
      test('Mockito example', () {
        final dbManager = MockDbManager();
    
        final mockTableDao = MockTableDao();
        // ↑ instantiate this mocked class which will be a field value inside dbManager
        
        // Mock the field access for dbManager.foodTableDao
        when(dbManager.foodTableDao).thenReturn(mockTableDao);
        //  ↑ You're missing this ↑ level of stubbing, so add this
    
        when(mockTableDao.searchFoods()).thenAnswer((realInvocation) => 'Cucumber');
        // ↑ define the stubbing at the mockTableDao level, not dbManger.mockTableDao.searchFoods
        
        expect(dbManager.foodTableDao.searchFoods(), 'Cucumber');
        // You can use foodTableDao field here, getting the mocked value as expected
      });
    }
    

    In your example above, you can't access dbManager.foodTableDao to set up a mock for dbManager.foodTableDao.searchFoods(), because dbManager.foodTableDao is null until you mock it. So this line:

    when(_dbManager.foodTableDao.searchFoods(searchQuery)).thenAnswer((realInvocation) async => getFoodTable());
    

    was throwing the null exception, not the expect test below. (The expect test was never reached.)


    The issue with _prefManager.user.name is the same I'm guessing. You need to mock User class and provide a when/return of MockUser for _prefManager.user in order to provide another level of when/return for _prefManager.user.name.

    It's like Mockception... you need to go another level deeper in your mocks. ;)