SKILL.md
readonlyread-only
name
dart-generate-test-mocks
description
Define and generate mock objects for external dependencies using `package:mockito` and `build_runner`. Use when unit testing classes that depend on complex external services like APIs or databases.
測試與模擬 Dart 應用程式
目錄
為可測試性建構程式碼
設計 Dart 類別以支援依賴注入。將複雜的外部依賴(如 API 客戶端或資料庫)隔離,以便在測試時可用模擬物件取代。
- 透過類別建構子注入外部服務(例如
http.Client)。 - 嚴格使用
Uri物件(透過Uri.parse(string))來表示 URL。 - 利用 Dart 的物件導向特性(類別、mixin)為外部互動定義清晰的介面。
管理相依套件
在 pubspec.yaml 檔案中設定必要的測試與程式碼產生套件。
- 使用
dart pub add http新增執行時期相依套件(例如package:http)。 - 使用
dart pub add dev:test dev:mockito dev:build_runner新增測試相依套件。 - 使用前綴匯入 HTTP 函式庫以避免命名空間衝突:
import 'package:http/http.dart' as http;。
產生模擬物件
使用 package:mockito 和 build_runner 自動產生模擬類別,用於固定情境與行為驗證。
- 一律使用
@GenerateNiceMocks註解(優於@GenerateMocks,以避免遺漏 stub 的例外)。 - 將註解放在測試檔案中,傳入
MockSpec<Type>()物件的列表。 - 使用
.mocks.dart副檔名匯入產生的檔案。 - 執行
build_runner以產生模擬檔案:dart run build_runner build。
實作單元測試
使用產生的模擬物件隔離受測系統。使用 package:test 來組織測試套件。
- Stubbing(設定模擬行為): 在與受測系統互動前設定模擬行為。
- 對同步方法使用
when(mock.method()).thenReturn(value)。 - 關鍵: 對回傳
Future或Stream的方法,務必使用thenAnswer((_) async => value)。絕不要對非同步回傳使用thenReturn。
- 對同步方法使用
- 驗證: 斷言受測系統與模擬物件的互動是否正確。
- 使用
verify(mock.method()).called(1)檢查精確的呼叫次數。 - 使用參數匹配器如
any、anyNamed或captureAny進行彈性驗證。
- 使用
工作流程:建立與執行模擬測試
使用以下檢查清單來實作並驗證模擬單元測試。
任務進度
- [ ] 1. 識別要模擬的外部依賴(例如
http.Client)。 - [ ] 2. 將依賴注入目標類別的建構子。
- [ ] 3. 建立測試檔案(例如
target_test.dart)並加入@GenerateNiceMocks([MockSpec<Dependency>()])。 - [ ] 4. 加入
part或import指令以匯入產生的.mocks.dart檔案。 - [ ] 5. 執行
dart run build_runner build以產生模擬類別。 - [ ] 6. 使用
group()和test()撰寫測試案例。 - [ ] 7. 使用
when()設定所需行為的 stub。 - [ ] 8. 執行目標方法。
- [ ] 9. 使用
verify()驗證互動,並使用expect()斷言結果。 - [ ] 10. 使用
dart test執行測試套件。
回饋迴圈:測試失敗
如果測試失敗或 build_runner 遇到錯誤:
- 執行驗證器: 執行
dart test或dart run build_runner build。 - 檢視錯誤: 檢查遺漏的 stub、參數匹配器不符,或產生檔案中的語法錯誤。
- 修正:
- 如果模擬方法拋出非預期的 null 錯誤,請確認你使用了
@GenerateNiceMocks。 - 如果非同步 stub 拋出
ArgumentError,請將thenReturn改為thenAnswer。 - 如果
build_runner失敗,請確認.mocks.dart的匯入路徑與檔案名稱完全相符。
- 如果模擬方法拋出非預期的 null 錯誤,請確認你使用了
- 重複直到所有測試通過。
範例
高擬真模擬與測試範例
1. 受測系統(lib/api_service.dart)
import 'dart:convert';
import 'package:http/http.dart' as http;
class ApiService {
final http.Client client;
ApiService(this.client);
Future<String> fetchData(String urlString) async {
final uri = Uri.parse(urlString);
final response = await client.get(uri);
if (response.statusCode == 200) {
return jsonDecode(response.body)['data'];
} else {
throw Exception('Failed to load data');
}
}
}
2. 測試實作(test/api_service_test.dart)
import 'package:test/test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:http/http.dart' as http;
import 'package:my_app/api_service.dart';
// 為 http.Client 產生模擬類別
@GenerateNiceMocks([MockSpec<http.Client>()])
import 'api_service_test.mocks.dart';
void main() {
group('ApiService', () {
late ApiService apiService;
late MockClient mockHttpClient;
setUp(() {
mockHttpClient = MockClient();
apiService = ApiService(mockHttpClient);
});
test('若 HTTP 呼叫成功則回傳資料', () async {
// 安排:使用 thenAnswer 設定非同步 HTTP GET 請求的 stub
when(mockHttpClient.get(any)).thenAnswer(
(_) async => http.Response('{"data": "Success"}', 200),
);
// 執行
final result = await apiService.fetchData('https://api.example.com/data');
// 斷言
expect(result, 'Success');
// 驗證模擬物件以正確的 Uri 被呼叫
verify(mockHttpClient.get(Uri.parse('https://api.example.com/data'))).called(1);
});
test('若 HTTP 呼叫回傳錯誤則拋出例外', () {
// 安排
when(mockHttpClient.get(any)).thenAnswer(
(_) async => http.Response('Not Found', 404),
);
// 執行與斷言
expect(
apiService.fetchData('https://api.example.com/data'),
throwsException,
);
});
});
}






