如何使用 Mockito 模拟 Flutter 中的静态方法?
How to mock a static method in Flutter with Mockito?
我有一个函数 fetchPosts()
的文件,它负责从服务器获取新帖子并将它们存储在本地 sqlite 数据库中。
按照 sqflite doc 上的建议,我将单个引用存储到我的数据库中。
这是我的 database.dart
文件的内容:
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.
}
然后我在我的 fetchPosts.dart
文件中使用它
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]);
}
}
在我的测试中,我如何verify
调用DBProvider.insert()
?
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 ?*/);
});
});
}
最终,我不得不重写我的 database.dart
以使其可测试/可模拟。
这是新文件:
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);
}
// ...
}
现在我可以使用与 http.Client 相同的技巧了。
谢谢@RémiRousselet
这个问题是很久以前的,但这里有另一个解决方案。您可以重构对要从 class“包装器”方法调用的静态函数的调用。这是我经常用来模拟对第三方服务的请求的模式。
让我举个例子。为简单起见,假设 Engine 有 3 个需要模拟的静态方法:brake() 和 accelerate() 以及 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();
}
}
}
现在您想模拟对引擎的所有调用,为此我们可以将代码重构为:
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();
}
}
在集成测试中,您现在可以模拟直接与静态方法交互的 3 个方法,但您现在可以测试您的主要方法。虽然您也可以在这里重构引擎 class 本身,但通常 class 会在第三方服务中。
这个例子不是基于Volkswagen scandal ;).
假设我们要测试 [TargetClass.someMethodCallOtherStaticMethod]
Class StaticMethodClass {
static int someStaticMethod() {};
}
Class TargetClass {
int someMethodCallOtherStaticMethod() {
return StaticMethodClass.someStaticMethod();
}
}
我们应该重构 [[TargetClass.someMethodCallOtherStaticMethod]] 进行测试,
像这样:
Class TargetClass {
int someMethodCallOtherStaticMethod({@visibleForTesting dynamic staticMethodClassForTesting}) {
if (staticMethodClassForTesting != null) {
return staticMethodClassForTesting.someStaticMethod();
} else {
return StaticMethodClass.someStaticMethod();
}
}
}
现在你可以这样写你的测试用例了:
// 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);
})
我有一个函数 fetchPosts()
的文件,它负责从服务器获取新帖子并将它们存储在本地 sqlite 数据库中。
按照 sqflite doc 上的建议,我将单个引用存储到我的数据库中。
这是我的 database.dart
文件的内容:
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.
}
然后我在我的 fetchPosts.dart
文件中使用它
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]);
}
}
在我的测试中,我如何verify
调用DBProvider.insert()
?
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 ?*/);
});
});
}
最终,我不得不重写我的 database.dart
以使其可测试/可模拟。
这是新文件:
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);
}
// ...
}
现在我可以使用与 http.Client 相同的技巧了。 谢谢@RémiRousselet
这个问题是很久以前的,但这里有另一个解决方案。您可以重构对要从 class“包装器”方法调用的静态函数的调用。这是我经常用来模拟对第三方服务的请求的模式。
让我举个例子。为简单起见,假设 Engine 有 3 个需要模拟的静态方法:brake() 和 accelerate() 以及 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();
}
}
}
现在您想模拟对引擎的所有调用,为此我们可以将代码重构为:
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();
}
}
在集成测试中,您现在可以模拟直接与静态方法交互的 3 个方法,但您现在可以测试您的主要方法。虽然您也可以在这里重构引擎 class 本身,但通常 class 会在第三方服务中。
这个例子不是基于Volkswagen scandal ;).
假设我们要测试 [TargetClass.someMethodCallOtherStaticMethod]
Class StaticMethodClass {
static int someStaticMethod() {};
}
Class TargetClass {
int someMethodCallOtherStaticMethod() {
return StaticMethodClass.someStaticMethod();
}
}
我们应该重构 [[TargetClass.someMethodCallOtherStaticMethod]] 进行测试, 像这样:
Class TargetClass {
int someMethodCallOtherStaticMethod({@visibleForTesting dynamic staticMethodClassForTesting}) {
if (staticMethodClassForTesting != null) {
return staticMethodClassForTesting.someStaticMethod();
} else {
return StaticMethodClass.someStaticMethod();
}
}
}
现在你可以这样写你的测试用例了:
// 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);
})