dart-generate-test-mocks

dart-generate-test-mocks

熱門

使用 `package:mockito` 和 `build_runner` 定義並產生外部依賴的模擬物件。適用於對依賴複雜外部服務(如 API 或資料庫)的類別進行單元測試時。

392星標
25分支
更新於 2026/7/10
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:mockitobuild_runner 自動產生模擬類別,用於固定情境與行為驗證。

  • 一律使用 @GenerateNiceMocks 註解(優於 @GenerateMocks,以避免遺漏 stub 的例外)。
  • 將註解放在測試檔案中,傳入 MockSpec<Type>() 物件的列表。
  • 使用 .mocks.dart 副檔名匯入產生的檔案。
  • 執行 build_runner 以產生模擬檔案:dart run build_runner build

實作單元測試

使用產生的模擬物件隔離受測系統。使用 package:test 來組織測試套件。

  • Stubbing(設定模擬行為): 在與受測系統互動前設定模擬行為。
    • 對同步方法使用 when(mock.method()).thenReturn(value)
    • 關鍵: 對回傳 FutureStream 的方法,務必使用 thenAnswer((_) async => value)。絕不要對非同步回傳使用 thenReturn
  • 驗證: 斷言受測系統與模擬物件的互動是否正確。
    • 使用 verify(mock.method()).called(1) 檢查精確的呼叫次數。
    • 使用參數匹配器如 anyanyNamedcaptureAny 進行彈性驗證。

工作流程:建立與執行模擬測試

使用以下檢查清單來實作並驗證模擬單元測試。

任務進度

  • [ ] 1. 識別要模擬的外部依賴(例如 http.Client)。
  • [ ] 2. 將依賴注入目標類別的建構子。
  • [ ] 3. 建立測試檔案(例如 target_test.dart)並加入 @GenerateNiceMocks([MockSpec<Dependency>()])
  • [ ] 4. 加入 partimport 指令以匯入產生的 .mocks.dart 檔案。
  • [ ] 5. 執行 dart run build_runner build 以產生模擬類別。
  • [ ] 6. 使用 group()test() 撰寫測試案例。
  • [ ] 7. 使用 when() 設定所需行為的 stub。
  • [ ] 8. 執行目標方法。
  • [ ] 9. 使用 verify() 驗證互動,並使用 expect() 斷言結果。
  • [ ] 10. 使用 dart test 執行測試套件。

回饋迴圈:測試失敗

如果測試失敗或 build_runner 遇到錯誤:

  1. 執行驗證器: 執行 dart testdart run build_runner build
  2. 檢視錯誤: 檢查遺漏的 stub、參數匹配器不符,或產生檔案中的語法錯誤。
  3. 修正:
    • 如果模擬方法拋出非預期的 null 錯誤,請確認你使用了 @GenerateNiceMocks
    • 如果非同步 stub 拋出 ArgumentError,請將 thenReturn 改為 thenAnswer
    • 如果 build_runner 失敗,請確認 .mocks.dart 的匯入路徑與檔案名稱完全相符。
  4. 重複直到所有測試通過。

範例

高擬真模擬與測試範例

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,
      );
    });
  });
}