Reputation: 2193
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 ?*/);
});
});
}
Upvotes: 26
Views: 17252
Reputation: 4759
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());
Upvotes: 0
Reputation: 127
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;
});
}
Upvotes: 0
Reputation: 1557
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);
})
Upvotes: 0
Reputation: 3090
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 ;).
Upvotes: 6
Reputation: 2193
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
Upvotes: 2