How to mock a static method in Flutter with Mockito?
Asked Answered
U

5

26

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 ?*/);
    });
  });
}
Uncourtly answered 4/10, 2019 at 14:34 Comment(4)
You cannot mock static members. If you need to mock then, don't make them staticWaspish
@RémiRousselet, thanks for the insight. I am curious, what's the reasoning behind not allowing static members to be mocked?Beard
Good question. I suspect it's out of convention with no reasoning. Object oriented devs like to punish fp devs for their choices.Caffeine
I guess is something more pointing to language limitations. But probably @ChristianFindlay has reasonJoost
U
2

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

Uncourtly answered 6/10, 2019 at 14:16 Comment(0)
D
6

The question was some while ago, but here is another solution. You can refactor calls to that static function to be called from a class "wrapper" method. This is a pattern I often use to mock requests to third party services.

Let me give you an example. To make it simple lets say Engine has 3 static methods that need to be mocked: brake() and accelerate() and speed().

class Car {
    int currentSpeed;

    void accelerateTo(int speed) {
         while(currentSpeed > speed) {
              Engine.brake();
              currentSpeed = Engine.speed();
         }
         while(currentSpeed < speed) {
              Engine.accelerate();
              currentSpeed = Engine.speed();
         }
    }
}

Now you want to mock all calls to the engine, to do so we could refactor the code to:

class Car {
    int currentSpeed;

    void accelerateTo(int speed) {
         while(currentSpeed > speed) {
              brake();
              currentSpeed = speed();
         }
         while(currentSpeed < speed) {
              accelerate();
              currentSpeed = speed();
         }
    }

    /// wrapper to mock Engine calls during test
    void brake() {
        Engine.brake();
    }

    /// wrapper to mock Engine calls during test
    int speed() {
        Engine.speed();
    }

    /// wrapper to mock Engine calls during test
    void accelerate() {
        Engine.accelerate();
    }

}

In the integration test you can now mock the 3 methods that interact with the static methods directly but you can now test your main method. While you could here also refactor the Engine class itself, often that class would be within a third party service.

This example is not based on the Volkswagen scandal ;).

Drucilla answered 10/3, 2021 at 7:55 Comment(0)
U
2

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

Uncourtly answered 6/10, 2019 at 14:16 Comment(0)
A
0

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); 
})

 
Ahoufe answered 15/4, 2021 at 3:16 Comment(5)
Please don't anyone ever do this.Diazomethane
While this does work, typically you try very hard not to leak knowledge of testing into the production code. So there are better patterns to use.Mackintosh
@ReedAbbott can you explain that how we can mock for this in a better way and why this one is not suitable to use via giving one answer example?Argentina
@Diazomethane any better code solution if you can provide?Argentina
@JayMungara The best answer is that you should either refactor the class with the static method to not use static methods or create a wrapper if you can't do that. Someone else already provided a wrapper solution, and it's the most upvoted answer for a good reason, though it should be a separate wrapper class. What you should never, never, ever do is add a bloody conditional to your production code that provides functionality that is only used by unit tests. Production code should never know anything about tests, but this is even worse, and it makes everything brittle and bug prone.Diazomethane
P
0

This is what worked for my case. I just created a method class field where I could insert a "mock" method:

class StaticMethodClass
{
  static int someStaticMethod() { return 0;}
}

class TargetClass
{

  static int Function() staticMethod = StaticMethodClass.someStaticMethod;

  int someMethodCallOtherStaticMethod() {
    return staticMethod();
  }
}

main() {
  test('test', () {
    TargetClass.staticMethod = () => 1;
  });
}
Pluto answered 26/3, 2023 at 4:18 Comment(0)
K
0

I had similar problem where I wanted to test the Analytics sent to FireBase. And we are using mocktail which doesn't support static method mocks

class AnalyticsSerrvice {
  static final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;

static Future<void> logEvent({
    required String name,
    Map<String, String>? params,
  }) async {
    await _analytics.logEvent(
      name: eventName,
      parameters: parameters,
    );
  }
}

The solution worked for me is also using the wrapper, but not a method wrapper, but a class wrapper.

So we created a

class DIAnalyticsService {
  Future<void> logEvent({
    required String name,
    Map<String, Object?>? params,
  }) async {
    await AnalyticsSerrvice.logEvent(name: eventName, params: parameters);
  }
}

And now this can be defined in the dependency injections and can be mocked.

the consuming widget class uses below to

locator<DIAnalyticsService>().logEvent(name: 'logged_out', params: {'key':'value'});

Now this can be tested.

class MockAnalyticsService extends Mock implements DIAnalyticsService {}

now a mock can be created

    final mockAnalyticService = MockAnalyticsService();
and required mocking can be setup
when(() => mockAnalyticService.logEvent(name: any(named: 'name'), params: any(named: 'params')))
            .thenAnswer((invocation) => Future.value());
Keelia answered 9/5 at 10:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.