使用 Mockito 模擬依賴項
有時,單元測試可能依賴於從即時 Web 服務或資料庫獲取資料的類。這在幾個方面不方便:
- 呼叫即時服務或資料庫會減慢測試執行速度。
- 如果 Web 服務或資料庫返回意外結果,則透過的測試可能會開始失敗。這被稱為“易變測試”。
- 使用即時 Web 服務或資料庫很難測試所有可能的成功和失敗場景。
因此,您可以透過“模擬”這些依賴項,而不是依賴於即時 Web 服務或資料庫。模擬允許模擬即時 Web 服務或資料庫,並根據情況返回特定結果。
總的來說,您可以透過建立類的替代實現來模擬依賴項。手動編寫這些替代實現,或者利用 Mockito 包 作為捷徑。
此食譜透過以下步驟演示了使用 Mockito 包進行模擬的基礎知識:
- 新增軟體包依賴項。
- 建立要測試的函式。
- 建立帶有模擬
http.Client的測試檔案。 - 為每種情況編寫測試。
- 執行測試。
有關更多資訊,請參閱 Mockito 包文件。
1. 新增軟體包依賴項
#要使用 mockito 包,請將其新增到 pubspec.yaml 檔案中,並將 flutter_test 依賴項新增到 dev_dependencies 部分。
此示例還使用了 http 包,因此請在 dependencies 部分定義該依賴項。
mockito: 5.0.0 透過程式碼生成支援 Dart 的空安全。要執行所需程式碼生成,請將 build_runner 依賴項新增到 dev_dependencies 部分。
要新增依賴項,請執行 flutter pub add。
flutter pub add http dev:mockito dev:build_runner2. 建立一個要測試的函式
#在此示例中,我們將單元測試來自 從網際網路獲取資料 食譜的 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 的檔案。
在 main 函式上新增 @GenerateMocks([http.Client]) 註釋,以使用 mockito 生成 MockClient 類。
生成的 MockClient 類實現了 http.Client 類。這允許您將 MockClient 傳遞給 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.
@GenerateMocks([http.Client])
void main() {
}接下來,透過執行以下命令生成模擬:
dart run build_runner build4. 為每種情況編寫測試
#fetchAlbum() 函式會執行以下兩項操作之一:
- 如果 http 呼叫成功,則返回一個
Album。 - 如果 http 呼叫失敗,則丟擲
Exception。
因此,您需要測試這兩種情況。使用 Mockito 提供的 when() 函式,為成功測試返回“Ok”響應,為不成功測試返回錯誤響應。
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);
});
});
}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.
@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);
});
});
}概述
#在此示例中,您已學習瞭如何使用 Mockito 測試依賴於 Web 服務或資料庫的函式或類。這只是 Mockito 庫和模擬概念的簡要介紹。有關更多資訊,請參閱 Mockito 包提供的文件。