使用 Mockito 模擬依賴
有時,單元測試可能會依賴從即時網路服務或資料庫擷取資料的類別。這有幾個不便之處
- 呼叫即時服務或資料庫會減慢測試執行速度。
- 如果網路服務或資料庫傳回非預期的結果,通過的測試可能會開始失敗。這被稱為「不穩定的測試」。
- 很難透過使用即時網路服務或資料庫來測試所有可能的成功和失敗情境。
因此,您可以「模擬」這些依賴項,而不是依賴即時網路服務或資料庫。模擬允許模擬即時網路服務或資料庫,並根據情況傳回特定結果。
一般來說,您可以透過建立類別的替代實作來模擬依賴項。手動編寫這些替代實作,或利用 Mockito 套件作為捷徑。
本食譜演示了使用 Mockito 套件進行模擬的基本知識,步驟如下
- 加入套件依賴。
- 建立要測試的函式。
- 建立一個帶有模擬
http.Client
的測試檔案。 - 為每個條件撰寫測試。
- 執行測試。
如需更多資訊,請參閱 Mockito 套件 文件。
1. 加入套件依賴
#若要使用 mockito
套件,請將其與 flutter_test
依賴項一起加入 pubspec.yaml
檔案的 dev_dependencies
區段。
此範例也使用 http
套件,因此請在 dependencies
區段中定義該依賴項。
由於程式碼產生,mockito: 5.0.0
支援 Dart 的空值安全。若要執行所需的程式碼產生,請在 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())
直接向 fetchAlbum
方法提供 http.Client
。http.Client()
會建立預設的 http.Client
。
3. 建立一個帶有模擬 http.Client
的測試檔案
#接下來,建立一個測試檔案。
依照 單元測試簡介 食譜中的建議,在根 test
資料夾中建立一個名為 fetch_album_test.dart
的檔案。
將註釋 @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 build
4. 為每個條件撰寫測試
#fetchAlbum()
函式會執行以下兩個動作之一
- 如果 http 呼叫成功,則傳回
Album
- 如果 http 呼叫失敗,則擲回
Exception
因此,您想要測試這兩個條件。使用 MockClient
類別針對成功測試傳回「確定」回應,並針對不成功的測試傳回錯誤回應。使用 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.
@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 來測試依賴於網路服務或資料庫的函式或類別。這只是對 Mockito 程式庫和模擬概念的簡短介紹。如需更多資訊,請參閱 Mockito 套件 提供的文件。
除非另有說明,否則本網站上的文件反映了 Flutter 的最新穩定版本。頁面上次更新於 2024-04-04。 檢視原始碼 或 回報問題。