使用 Mockito 模拟依赖项
使用 Mockito 软件包来模仿服务行为以进行测试。
有时,单元测试可能依赖于从实时 Web 服务或数据库中获取数据的类。由于以下几个原因,这并不方便:
- 调用实时服务或数据库会拖慢测试执行速度。
- 如果 Web 服务或数据库返回意外结果,通过的测试可能会开始失败。这被称为“脆弱的测试”(flaky test)。
- 使用实时 Web 服务或数据库很难测试所有可能的成功和失败场景。
因此,您可以“模拟”(mock)这些依赖项,而不是依赖于实时 Web 服务或数据库。模拟允许您仿真实时 Web 服务或数据库,并根据具体情况返回预期的结果。
一般来说,您可以通过创建类的替代实现来模拟依赖项。您可以手动编写这些替代实现,或者利用 Mockito 软件包 作为快捷方式。
本指南通过以下步骤演示了使用 Mockito 软件包进行模拟的基础知识:
- 添加软件包依赖。
- 创建用于测试的函数。
- 使用模拟的
http.Client创建测试文件。 - 为每种情况编写测试。
- 运行测试。
有关更多信息,请参阅 Mockito 软件包 文档。
1. 添加软件包依赖
#要使用 mockito 软件包,请将其与 flutter_test 依赖项一起添加到 pubspec.yaml 文件的 dev_dependencies 部分。
此示例还使用了 http 软件包,因此请在 dependencies 部分定义该依赖项。
mockito: 5.0.0 通过代码生成支持 Dart 的空安全(null safety)。要运行所需的代码生成,请在 dev_dependencies 部分添加 build_runner 依赖项。
要添加依赖项,请运行 flutter pub add
flutter pub add http dev:mockito dev:build_runner
2. 创建用于测试的函数
#在本例中,我们将对来自 从互联网获取数据 指南中的 fetchAlbum 函数进行单元测试。要测试此函数,需要做两处修改:
- 向函数提供一个
http.Client。这允许根据情况提供正确的http.Client。对于 Flutter 和服务端项目,提供http.IOClient。对于浏览器应用,提供http.BrowserClient。对于测试,提供一个模拟的http.Client。 - 使用提供的
client从互联网获取数据,而不是使用难以模拟的静态http.get()方法。
该函数现在应该如下所示:
Future<Album> fetchAlbum(http.Client client) async {
final response = await client.get(
Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
);
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load album');
}
}
在您的应用代码中,您可以直接通过 fetchAlbum(http.Client()) 将 http.Client 提供给 fetchAlbum 方法。http.Client() 会创建一个默认的 http.Client。
3. 使用模拟的 http.Client 创建测试文件
#
接下来,创建一个测试文件。
按照 单元测试简介 指南中的建议,在根目录的 test 文件夹中创建一个名为 fetch_album_test.dart 的文件。
在主函数上添加 @GenerateMocks([], customMocks: [MockSpec<http.Client>(as: #MockHttpClient)]) 注解,以使用 mockito 生成 MockHttpClient 类。
生成的 MockHttpClient 类实现了 http.Client 类。这允许您将 MockHttpClient 传递给 fetchAlbum 函数,并在每个测试中返回不同的 HTTP 响应。
生成的模拟代码将位于 fetch_album_test.mocks.dart 中。导入此文件以使用它们。
import 'package:http/http.dart' as http;
import 'package:mocking/main.dart';
import 'package:mockito/annotations.dart';
// Generate a MockClient using the Mockito package.
// Create new instances of this class in each test.
// Note: Naming the generated mock `MockHttpClient` to avoid confusion with
// `MockClient` from `package:http/testing.dart`.
@GenerateMocks([], customMocks: [MockSpec<http.Client>(as: #MockHttpClient)])
void main() {
}
接下来,运行以下命令来生成模拟:
dart run build_runner build
4. 为每种情况编写测试
#fetchAlbum() 函数执行以下两种操作之一:
- 如果 HTTP 调用成功,则返回一个
Album。 - 如果 HTTP 调用失败,则抛出一个
Exception。
因此,您需要测试这两种情况。使用 MockHttpClient 类在成功测试中返回“Ok”响应,在失败测试中返回错误响应。使用 Mockito 提供的 when() 函数来测试这些条件。
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:mocking/main.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'fetch_album_test.mocks.dart';
// Generate a MockClient using the Mockito package.
// Create new instances of this class in each test.
// Note: Naming the generated mock `MockHttpClient` to avoid confusion with
// `MockClient` from `package:http/testing.dart`.
@GenerateMocks([], customMocks: [MockSpec<http.Client>(as: #MockHttpClient)])
void main() {
group('fetchAlbum', () {
test('returns an Album if the http call completes successfully', () async {
final client = MockHttpClient();
// Use Mockito to return a successful response when it calls the
// provided http.Client.
when(
client.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')),
).thenAnswer(
(_) async =>
http.Response('{"userId": 1, "id": 2, "title": "mock"}', 200),
);
expect(await fetchAlbum(client), isA<Album>());
});
test('throws an exception if the http call completes with an error', () {
final client = MockHttpClient();
// Use Mockito to return an unsuccessful response when it calls the
// provided http.Client.
when(
client.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')),
).thenAnswer((_) async => http.Response('Not Found', 404));
expect(fetchAlbum(client), throwsException);
});
});
}
5. 运行测试
#既然您已经编写好了带有测试的 fetchAlbum() 函数,现在就可以运行测试了。
flutter test test/fetch_album_test.dart
您还可以按照 单元测试简介 指南中的说明,在您喜爱的编辑器中运行测试。
完整示例
#lib/main.dart
#import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Future<Album> fetchAlbum(http.Client client) async {
final response = await client.get(
Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
);
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load album');
}
}
class Album {
final int userId;
final int id;
final String title;
const Album({required this.userId, required this.id, required this.title});
factory Album.fromJson(Map<String, dynamic> json) {
return Album(
userId: json['userId'] as int,
id: json['id'] as int,
title: json['title'] as String,
);
}
}
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late final Future<Album> futureAlbum;
@override
void initState() {
super.initState();
futureAlbum = fetchAlbum(http.Client());
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Fetch Data Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: Scaffold(
appBar: AppBar(title: const Text('Fetch Data Example')),
body: Center(
child: FutureBuilder<Album>(
future: futureAlbum,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data!.title);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
// By default, show a loading spinner.
return const CircularProgressIndicator();
},
),
),
),
);
}
}
test/fetch_album_test.dart
#import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:mocking/main.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'fetch_album_test.mocks.dart';
// Generate a MockClient using the Mockito package.
// Create new instances of this class in each test.
// Note: Naming the generated mock `MockHttpClient` to avoid confusion with
// `MockClient` from `package:http/testing.dart`.
@GenerateMocks([], customMocks: [MockSpec<http.Client>(as: #MockHttpClient)])
void main() {
group('fetchAlbum', () {
test('returns an Album if the http call completes successfully', () async {
final client = MockHttpClient();
// Use Mockito to return a successful response when it calls the
// provided http.Client.
when(
client.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')),
).thenAnswer(
(_) async =>
http.Response('{"userId": 1, "id": 2, "title": "mock"}', 200),
);
expect(await fetchAlbum(client), isA<Album>());
});
test('throws an exception if the http call completes with an error', () {
final client = MockHttpClient();
// Use Mockito to return an unsuccessful response when it calls the
// provided http.Client.
when(
client.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')),
).thenAnswer((_) async => http.Response('Not Found', 404));
expect(fetchAlbum(client), throwsException);
});
});
}
概述
#在本示例中,您学习了如何使用 Mockito 来测试依赖于 Web 服务或数据库的函数或类。这只是对 Mockito 库和模拟概念的简短介绍。有关更多信息,请参阅 Mockito 软件包 提供的文档。