使用 Mockito 模拟依赖关系
某些情况下,单元测试可能会依赖需要从线上 Web 服务或数据库中获取数据的类。这样会带来一些不便,原因如下:
Sometimes, unit tests might depend on classes that fetch data from live web services or databases. This is inconvenient for a few reasons:
-
访问线上服务或数据库会拖慢测试执行效率。
Calling live services or databases slows down test execution.
-
原本可以通过的测试可能会失败,因为 Web 服务或数据库可能会返回不符合预期的结果。这种情况被称作“flaky test”。
A passing test might start failing if a web service or database returns unexpected results. This is known as a “flaky test.”
-
使用线上 web 服务或数据库来测试很难覆盖全所有可能成功或失败的场景。
It is difficult to test all possible success and failure scenarios by using a live web service or database.
因此,最好不要依赖线上 web 服务或数据库,我们可以把这些依赖“模拟(mock)”出来。模拟(Mocks)允许我们仿造一个线上服务或数据库,并且可以根据条件返回特定结果。
Therefore, rather than relying on a live web service or database, you can “mock” these dependencies. Mocks allow emulating a live web service or database and return specific results depending on the situation.
通常来说,可以通过创建类的另一种实现来模拟(mock)这种依赖。类的另一种实现可以手写,也可以借助 Mockito 包,后者简单一些。
Generally speaking, you can mock dependencies by creating an alternative implementation of a class. Write these alternative implementations by hand or make use of the Mockito package as a shortcut.
本篇教程介绍了 Mockito 包的基本用法,可以参考以下步骤:
This recipe demonstrates the basics of mocking with the Mockito package using the following steps:
使用步骤
Directions
-
添加
mockito
和test
依赖Add the package dependencies.
-
创建一个要测试的函数
Create a function to test.
-
创建一个模拟了
http.Client
的测试文件Create a test file with a mock
http.Client
. -
给每一个条件写一个测试
Write a test for each condition.
-
执行这些测试
Run the tests.
更多信息可以查阅 Mockito package 的官方文档。
For more information, see the Mockito package documentation.
1. 添加 package 依赖
1. Add the package dependencies
为了使用 mockito 包,首先将其和 flutter_test
的依赖一起添加到 pubspec.yaml
文件的 dev_dependencies
部分:
To use the mockito
package, add it to the
pubspec.yaml
file along with the flutter_test
dependency in the
dev_dependencies
section.
本例中还使用了 http
包,需要添加到 dependencies
部分:
This example also uses the http
package,
so define that dependency in the dependencies
section.
感谢代码生成,mockito: 5.0.0
已经支持了 Dart 的空安全。要运行所需的代码生成工具,请将 build_runner
依赖添加到 dev_dependencies
项目下。
mockito: 5.0.0
supports Dart’s null safety thanks to code generation.
To run the required code generation, add the build_runner
dependency
in the dev_dependencies
section.
dependencies:
http: <newest_version>
dev_dependencies:
flutter_test:
sdk: flutter
mockito: <newest_version>
build_runner: <newest_version>
2. 创建一个要测试的函数
2. Create a function to test
本例中,我们要对 获取网络数据
章节的 fetchAlbum
函数进行单元测试。为了便于测试,我们需要做两个改动:
In this example, unit test the fetchAlbum
function from the
Fetch data from the internet recipe.
To test this function, make two changes:
-
给函数提供一个
http.Client
。这样的话我们可以在不同情形下提供相应的http.Client
实例。如果是 Flutter 以及服务端项目,可以提供http.IOClient
。如果是浏览器应用,可以提供http.BrowserClient
。为了测试,我们要提供一个模拟的http.Client
。Provide an
http.Client
to the function. This allows providing the correcthttp.Client
depending on the situation. For Flutter and server-side projects, provide anhttp.IOClient
. For Browser apps, provide anhttp.BrowserClient
. For tests, provide a mockhttp.Client
. -
使用上面提供的
client
来请求网络数据,不要用http.get()
这个静态方法,因为它比较难以模拟。Use the provided
client
to fetch data from the internet, rather than the statichttp.get()
method, which is difficult to mock.
函数经过改动之后:
The function should now look like this:
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)); } else { // If the server did not return a 200 OK response, // then throw an exception. throw Exception('Failed to load album'); } }
In your app code, you can provide an http.Client
to the fetchAlbum
method
directly with fetchAlbum(http.Client())
. http.Client()
creates a default
http.Client
.
3. 创建一个模拟了 http.Client 的测试文件
http.Client
3. Create a test file with a mock 接下来,创建一个测试文件。遵循 单元测试介绍 章节的建议,我们在根目录下的 test
文件夹中创建一个名字为 fetch_post_test.dart
的文件。
Next, create a test file.
Following the advice in the Introduction to unit testing recipe,
create a file called fetch_album_test.dart
in the root test
folder.
在 main 函数上添加一个 @GenerateMocks([http.Client])
注解以生成含有 mockito
的 MockClient
类。
Add the annotation @GenerateMocks([http.Client])
to the main
function to generate a MockClient
class with mockito
.
MockClient
类实现了 http.Client
类。如此一来,我们就可以把 MockClient
传给 fetchPost
函数,还可以在每个测试中返回不同的 http 请求结果。
The generated MockClient
class implements the http.Client
class.
This allows you to pass the MockClient
to the fetchAlbum
function,
and return different http responses in each test.
生成的 mock 文件将会放在 fetch_album_test.mocks.dart
,请导入以使用它。
The generated mocks will be located in fetch_album_test.mocks.dart
.
Import this file to use them.
import 'package:http/http.dart' as http; import 'package:mockito/annotations.dart'; // Generate a MockClient using the Mockito package. // Create new instances of this class in each test. @GenerateMocks([http.Client]) void main() { }
Next, generate the mocks running the following command:
$ flutter pub run build_runner build
4. 给每一个条件写一个测试
4. Write a test for each condition
回过头来看,fetchPost()
函数会完成下面两件事中的一件:
The fetchAlbum()
function does one of two things:
-
如果 http 请求成功,返回
Post
Returns an
Album
if the http call succeeds -
如果 http 请求失败,抛出
Exception
Throws an
Exception
if the http call fails
因此,我们要测试这两种条件。可以使用 MockClient
类为成功的测试返回一个 “OK” 的请求结果,为不成功的测试返回一个错误的请求结果。
Therefore, you want to test these two conditions.
Use the MockClient
class to return an “Ok” response
for the success test, and an error response for the unsuccessful test.
我们使用 Mockito 的 when()
函数来达到以上目的:
Test these conditions using the when()
function provided by
Mockito:
import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:mocking/main.dart'; import 'fetch_album_test.mocks.dart'; // Generate a MockClient using the Mockito package. // Create new instances of this class in each test. @GenerateMocks([http.Client]) void main() { group('fetchAlbum', () { test('returns an Album if the http call completes successfully', () async { final client = MockClient(); // 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 = MockClient(); // 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. Run the tests
5. 执行测试
现在我们有了一个带测试的 fetchAlbum()
函数,开始执行测试!
Now that you have a fetchAlbum()
function with tests in place,
run the tests.
$ flutter test test/fetch_album_test.dart
你也可以参考 单元测试介绍 章节用自己喜欢的编辑器来执行测试。
You can also run tests inside your favorite editor by following the instructions in the Introduction to unit testing recipe.
完整的样例
Complete example
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)); } 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'], id: json['id'], title: json['title'], ); } } void main() => runApp(const MyApp()); class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override _MyAppState 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( primarySwatch: Colors.blue, ), 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:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:mocking/main.dart'; import 'fetch_album_test.mocks.dart'; // Generate a MockClient using the Mockito package. // Create new instances of this class in each test. @GenerateMocks([http.Client]) void main() { group('fetchAlbum', () { test('returns an Album if the http call completes successfully', () async { final client = MockClient(); // 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 = MockClient(); // 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); }); }); }
Summary
总结
通过本例,我们已经学会了如何用 Mockito 来测试对 web 服务或数据库有依赖的函数或类。这里只是简短地介绍了 Mockito 库以及模拟(mocking)的概念。更多内容请移步至 Mockito package。
In this example, you’ve learned how to use Mockito to test functions or classes that depend on web services or databases. This is only a short introduction to the Mockito library and the concept of mocking. For more information, see the documentation provided by the Mockito package.