dart-generate-test-mocks

dart-generate-test-mocks

熱門

使用 `package:mockito` 與 `build_runner` 定義並產生外部相依項目的 Mock 物件。適用於對相依 API 或資料庫等複雜外部服務的類別進行單元測試的場景。

2792星標
164分支
更新於 2026/8/5
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:mockitobuild_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)
    • 關鍵指示: 若方法回傳 FutureStream,請務必使用 thenAnswer((_) async => value)。切勿對非同步回傳使用 thenReturn
  • Verification(驗證互動): 斷言(Assert)受測系統是否正確地與 Mock 物件進行了互動。
    • 使用 verify(mock.method()).called(1) 來檢查確切的呼叫次數。
    • 使用如 anyanyNamedcaptureAny 等參數比對器(Argument matchers)進行彈性驗證。

工作流程:建立並執行 Mock 測試

請使用以下核取清單來實作與驗證帶有 Mock 的單元測試。

任務進度

  • [ ] 1. 確認要進行 Mock 的外部相依項目(例如 http.Client)。
  • [ ] 2. 將該相依項目注入至目標類別的建構子中。
  • [ ] 3. 建立測試檔案(例如 target_test.dart)並加入 @GenerateNiceMocks([MockSpec<Dependency>()])
  • [ ] 4. 為產生的 .mocks.dart 檔案新增 partimport 指示詞。
  • [ ] 5. 執行 dart run build_runner build 以產生 Mock 類別。
  • [ ] 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. 修復問題:
    • 若 Mock 方法拋出非預期的 null 錯誤,請確認是否已使用 @GenerateNiceMocks
    • 若非同步 Stub 拋出 ArgumentError,請將 thenReturn 改為 thenAnswer
    • build_runner 失敗,請確認 .mocks.dart 的匯入路徑與檔名完全一致。
  4. 重複上述步驟直到所有測試通過。

範例

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