Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock a static method in Flutter with Mockito?

I have a file a function fetchPosts() which is in charge of getting new Posts from a server and store them in a local sqlite database.

As recommended on the sqflite doc, I store a single ref to my database.

Here is the content of my database.dart file:

import 'dart:async';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';

class DBProvider {
  DBProvider._();
  static final DBProvider db = DBProvider._();

  static Database _database;

  static Future<Database> get database async {
    if (_database != null) return _database;
    // if _database is null, we instantiate it
    _database = await _initDB();
    return _database;
  }

  static Future<Database> _initDB() async {
    final dbPath = await getDatabasesPath();
    String path = join(dbPath, 'demo.db');

    return await openDatabase(path, version: 1, onCreate: _onCreate);
  }

  static Future<String> insert(String table, Map<String, dynamic> values) async { /* insert the record*/ }

  // Other functions like update, delete etc.
}

Then I use it as such in my fetchPosts.dart file

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../services/database.dart';

const url = 'https://myapp.herokuapp.com';

Future<void> fetchPosts() {
  final client = http.Client();
  return fetchPostsUsingClient(client);
}

Future<void> fetchPostsUsingClient(http.Client client) async {
  final res = await client.get(url);
  final posts await Post.fromJson(json.decode(response.body));

  for (var i = 0; i < posts.length; i++) {
    await DBProvider.insert('posts', posts[i]);
  }
}

In my test, how can I verify that DBProvider.insert() has been called?

fetchPosts_test.dart

import 'package:test/test.dart';
import 'package:http/http.dart' as http;
import 'package:mockito/mockito.dart';
import 'package:../services/fetchPosts.dart';

// Create a MockClient using the Mock class provided by the Mockito package.
// Create new instances of this class in each test.
class MockClient extends Mock implements http.Client {}

void main() {
  group('fetchPosts', () {
    test('update local db', () async {
      final client = MockClient();

      // Use Mockito to return a successful response when it calls the provided http.Client.
      when(client.get()).thenAnswer((_) async => http.Response('{"title": "Test"}', 200));

      await fetchPostsWithClient(client);

      verify(/* DBProvider.insert has been called ?*/);
    });
  });
}
like image 312
Gpack Avatar asked Oct 04 '19 14:10

Gpack


People also ask

Can Mockito mock static methods?

Mockito allows us to create mock objects. Since static method belongs to the class, there is no way in Mockito to mock static methods. However, we can use PowerMock along with Mockito framework to mock static methods.

Which Mockito framework is used to mock the static method?

As previously mentioned, since Mockito 3.4. 0, we can use the Mockito. mockStatic(Class<T> classToMock) method to mock invocations to static method calls. This method returns a MockedStatic object for our type, which is a scoped mock object.

Can we mock static private methods?

For Mockito, there is no direct support to mock private and static methods. In order to test private methods, you will need to refactor the code to change the access to protected (or package) and you will have to avoid static/final methods.


2 Answers

Eventually, I had to rewrite my database.dart to make it testable / mockable.
Here's the new file:

import 'dart:async';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';

class DBProvider {
  static final DBProvider _singleton = DBProvider._internal();

  factory DBProvider() {
    return _singleton;
  }

  DBProvider._internal();

  static Database _db;

  static Future<Database> _getDatabase() async {
    if (_db != null) return _db;
    // if _database is null, we instantiate it
    _db = await _initDB();
    return _db;
  }

  static Future<Database> _initDB() async {
    final dbPath = await getDatabasesPath();
    String path = join(dbPath, 'demo.db');

    return openDatabase(path, version: 1, onCreate: _onCreate);
  }

  Future<String> insert(String table, Map<String, dynamic> values) async {
    final db = await _getDatabase();
    return db.insert(table, values);
  }

  // ...
}

Now I can use the same trick as with the http.Client. Thank you @RémiRousselet

like image 84
Gpack Avatar answered Sep 17 '22 09:09

Gpack


Let's say we want to test [TargetClass.someMethodCallOtherStaticMethod]

Class StaticMethodClass {
  static int someStaticMethod() {};
}

Class TargetClass {
  int someMethodCallOtherStaticMethod() {
    return StaticMethodClass.someStaticMethod();
  }
}

We should refactor [[TargetClass.someMethodCallOtherStaticMethod]] for testing, like this:

Class TargetClass {
  int someMethodCallOtherStaticMethod({@visibleForTesting dynamic staticMethodClassForTesting}) {
    if (staticMethodClassForTesting != null) {
      return staticMethodClassForTesting.someStaticMethod();
    } else {
      return StaticMethodClass.someStaticMethod();
    }        
  }
}

Now you can write your test case like this:

// MockClass need to implement nothing, just extends Mock
MockClass extends Mock {}
test('someMethodCallOtherStaticMethod', () {
  // We MUST define `mocked` as a dynamic type, so that no errors will be reported during compilation
  dynamic mocked = MockClass();
  TargetClass target = TargetClass();
  when(mocked.someStaticMethod()).thenAnswer((realInvocation) => 42);
  expect(target.someMethodCallOtherStaticMethod(staticMethodClassForTesting: mocked), 42); 
})

 
like image 26
DongXu Avatar answered Sep 19 '22 09:09

DongXu