模拟依赖
使用 Mockito 模拟依赖关系
实用教程chevron_right测试 (Testing)chevron_right单元测试 (Unit)chevron_rightMocking
某些情况下,单元测试可能会依赖需要从线上 Web 服务或数据库中获取数据的类。这样会带来一些不便,原因如下:
- 访问线上服务或数据库会拖慢测试执行效率。
- 原本可以通过的测试可能会失败,因为 Web 服务或数据库可能会返回不符合预期的结果。这种情况被称作“flaky test”。
- 使用线上 web 服务或数据库来测试很难覆盖全所有可能成功或失败的场景。
因此,最好不要依赖线上 web 服务或数据库,我们可以把这些依赖“模拟(mock)”出来。模拟(Mocks)允许我们仿造一个线上服务或数据库,并且可以根据条件返回特定结果。
通常来说,可以通过创建类的另一种实现来模拟(mock)这种依赖。类的另一种实现可以手写,也可以借助 Mockito 包,后者简单一些。
本篇教程介绍了 Mockito 包的基本用法,可以参考以下步骤:
使用步骤
- 添加 mockito和test依赖
- 创建一个要测试的函数
- 创建一个模拟了 http.Client的测试文件
- 给每一个条件写一个测试
- 执行这些测试
更多信息可以查阅 Mockito package 的官方文档。
1. 添加 package 依赖
为了使用 mockito 包,首先将其和 flutter_test 的依赖一起添加到 pubspec.yaml 文件的 dev_dependencies 部分:
本例中还使用了 http 包,需要添加到 dependencies 部分:
感谢代码生成,mockito: 5.0.0 已经支持了 Dart 的空安全。要运行所需的代码生成工具,请将 build_runner 依赖添加到 dev_dependencies 项目下。
运行 flutter pub add 添加依赖:
flutter pub add http dev:mockito dev:build_runner
content_copy
2. 创建一个要测试的函数
本例中,我们要对 获取网络数据 章节的 fetchAlbum 函数进行单元测试。为了便于测试,我们需要做两个改动:
- 给函数提供一个 http.Client。这样的话我们可以在不同情形下提供相应的http.Client实例。如果是 Flutter 以及服务端项目,可以提供http.IOClient。如果是浏览器应用,可以提供http.BrowserClient。为了测试,我们要提供一个模拟的http.Client。
- 使用上面提供的 client来请求网络数据,不要用http.get()这个静态方法,因为它比较难以模拟。
函数经过改动之后:
dart
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');
  }
}
content_copy
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 的测试文件
接下来,创建一个测试文件。
遵循 单元测试介绍 章节的建议,我们在根目录下的 test 文件夹中创建一个名字为 fetch_post_test.dart 的文件。
在 main 函数上添加一个 @GenerateMocks([http.Client]) 注解以生成含有 mockito 的 MockClient 类。
MockClient 类实现了 http.Client 类。如此一来,我们就可以把 MockClient 传给 fetchPost 函数,还可以在每个测试中返回不同的 http 请求结果。
生成的 mock 文件将会放在 fetch_album_test.mocks.dart,请导入以使用它。
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.
@GenerateMocks([http.Client])
void main() {
}
content_copy
Next, generate the mocks running the following command:
dart run build_runner build
content_copy
4. 给每一个条件写一个测试
回过头来看,fetchPost() 函数会完成下面两件事中的一件:
- 如果 http 请求成功,返回 Post
- 如果 http 请求失败,抛出 Exception
因此,我们要测试这两种条件。可以使用 MockClient 类为成功的测试返回一个 "OK" 的请求结果,为不成功的测试返回一个错误的请求结果。
我们使用 Mockito 的 when() 函数来达到以上目的:
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.
@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);
    });
  });
}
content_copy
5. 执行测试
现在我们有了一个带测试的 fetchAlbum() 函数,开始执行测试!
flutter test test/fetch_album_test.dart
content_copy
你也可以参考 单元测试介绍 章节用自己喜欢的编辑器来执行测试。
完整的样例
lib/main.dart
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();
            },
          ),
        ),
      ),
    );
  }
}
content_copy
test/fetch_album_test.dart
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.
@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);
    });
  });
}
content_copy
总结
通过本例,我们已经学会了如何用 Mockito 来测试对 web 服务或数据库有依赖的函数或类。这里只是简短地介绍了 Mockito 库以及模拟(mocking)的概念。更多内容请移步至 Mockito package。
 
                    
                     
                    
                 
                    
                 
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号