SKILL.md
唯讀
名稱
dart-generate-test-mocks
描述
使用 `package:mockito` 與 `build_runner` 定義並產生外部相依項目的 Mock 物件。適用於對相依 API 或資料庫等複雜外部服務的類別進行單元測試的場景。
Dart 應用程式測試與 Mock 實作
目錄
調整程式碼結構以提升可測試性
設計 Dart 類別時應支援相依注入(Dependency Injection)。將複雜的外部相依項目(例如 API 用戶端或資料庫)隔離,以便在測試期間替換為 Mock 物件。
- 透過類別建構子注入外部服務(例如
http.Client)。 - 嚴格使用
Uri.parse(string)將 URL 表示為Uri物件。 - 善用 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;。
產生 Mock 物件
使用 package:mockito 與 build_runner 自動產生 Mock 類別,用於固定情境與行為驗證。
- 務必使用
@GenerateNiceMocks註解(優於@GenerateMocks,可避免遺漏 Stub 時拋出例外)。 - 將註解放在測試檔案中,傳入
MockSpec<Type>()物件清單。 - 使用
.mocks.dart副檔名匯入產生的檔案。 - 執行
build_runner以產生 Mock 檔案:dart run build_runner build。
實作單元測試
使用產生的 Mock 物件隔離受測系統(System Under Test)。使用 package:test 來組織測試套件。
- Stubbing(設定預期行為): 在與受測系統互動前設定 Mock 的行為。
- 同步方法請使用
when(mock.method()).thenReturn(value)。 - 關鍵指示: 若方法回傳
Future或Stream,請務必使用thenAnswer((_) async => value)。切勿對非同步回傳使用thenReturn。
- 同步方法請使用
- Verification(驗證互動): 斷言(Assert)受測系統是否正確地與 Mock 物件進行了互動。
- 使用
verify(mock.method()).called(1)來檢查確切的呼叫次數。 - 使用如
any、anyNamed或captureAny等參數比對器(Argument matchers)進行彈性驗證。
- 使用
工作流程:建立並執行 Mock 測試
請使用以下核取清單來實作與驗證帶有 Mock 的單元測試。
任務進度
- [ ] 1. 確認要進行 Mock 的外部相依項目(例如
http.Client)。 - [ ] 2. 將該相依項目注入至目標類別的建構子中。
- [ ] 3. 建立測試檔案(例如
target_test.dart)並加入@GenerateNiceMocks([MockSpec<Dependency>()])。 - [ ] 4. 為產生的
.mocks.dart檔案新增part或import指示詞。 - [ ] 5. 執行
dart run build_runner build以產生 Mock 類別。 - [ ] 6. 使用
group()與test()撰寫測試用例。 - [ ] 7. 使用
when()設定所需的 Stub 行為。 - [ ] 8. 執行目標方法。
- [ ] 9. 使用
verify()驗證互動,並使用expect()斷言結果。 - [ ] 10. 使用
dart test執行測試套件。
回饋迴圈:測試失敗處理
若測試失敗或 build_runner 遇到錯誤:
- 執行驗證命令: 執行
dart test或dart run build_runner build。 - 檢視錯誤訊息: 檢查是否有遺漏的 Stub、參數比對器不吻合,或產生的檔案中存在語法錯誤。
- 修復問題:
- 若 Mock 方法拋出非預期的 null 錯誤,請確認是否已使用
@GenerateNiceMocks。 - 若非同步 Stub 拋出
ArgumentError,請將thenReturn改為thenAnswer。 - 若
build_runner失敗,請確認.mocks.dart的匯入路徑與檔名完全一致。
- 若 Mock 方法拋出非預期的 null 錯誤,請確認是否已使用
- 重複上述步驟直到所有測試通過。
範例
高擬真 Mock 與測試範例
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';
// Generate the mock class for 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('returns data if the http call completes successfully', () async {
// Arrange: Stub the async HTTP GET request using thenAnswer
when(mockHttpClient.get(any)).thenAnswer(
(_) async => http.Response('{"data": "Success"}', 200),
);
// Act
final result = await apiService.fetchData('https://api.example.com/data');
// Assert
expect(result, 'Success');
// Verify the mock was called with the correct Uri
verify(mockHttpClient.get(Uri.parse('https://api.example.com/data'))).called(1);
});
test('throws an exception if the http call completes with an error', () {
// Arrange
when(mockHttpClient.get(any)).thenAnswer(
(_) async => http.Response('Not Found', 404),
);
// Act & Assert
expect(
apiService.fetchData('https://api.example.com/data'),
throwsException,
);
});
});
}






