跳到主內容

使用 Mockito 模擬依賴項

使用 Mockito 軟體包來模仿服務行為以進行測試。

有時,單元測試可能依賴於從即時 Web 服務或資料庫中獲取資料的類。由於以下幾個原因,這並不方便:

  • 呼叫即時服務或資料庫會拖慢測試執行速度。
  • 如果 Web 服務或資料庫返回意外結果,透過的測試可能會開始失敗。這被稱為“脆弱的測試”(flaky test)。
  • 使用即時 Web 服務或資料庫很難測試所有可能的成功和失敗場景。

因此,您可以“模擬”(mock)這些依賴項,而不是依賴於即時 Web 服務或資料庫。模擬允許您模擬即時 Web 服務或資料庫,並根據具體情況返回預期的結果。

一般來說,您可以透過建立類的替代實現來模擬依賴項。您可以手動編寫這些替代實現,或者利用 Mockito 軟體包 作為快捷方式。

本指南透過以下步驟演示了使用 Mockito 軟體包進行模擬的基礎知識:

  1. 新增軟體包依賴。
  2. 建立用於測試的函式。
  3. 使用模擬的 http.Client 建立測試檔案。
  4. 為每種情況編寫測試。
  5. 執行測試。

有關更多資訊,請參閱 Mockito 軟體包 文件。

1. 新增軟體包依賴

#

要使用 mockito 軟體包,請將其與 flutter_test 依賴項一起新增到 pubspec.yaml 檔案的 dev_dependencies 部分。

此示例還使用了 http 軟體包,因此請在 dependencies 部分定義該依賴項。

mockito: 5.0.0 透過程式碼生成支援 Dart 的空安全(null safety)。要執行所需的程式碼生成,請在 dev_dependencies 部分新增 build_runner 依賴項。

要新增依賴項,請執行 flutter pub add

flutter pub add http dev:mockito dev:build_runner

2. 建立用於測試的函式

#

在本例中,我們將對來自 從網際網路獲取資料 指南中的 fetchAlbum 函式進行單元測試。要測試此函式,需要做兩處修改:

  1. 向函式提供一個 http.Client。這允許根據情況提供正確的 http.Client。對於 Flutter 和服務端專案,提供 http.IOClient。對於瀏覽器應用,提供 http.BrowserClient。對於測試,提供一個模擬的 http.Client
  2. 使用提供的 client 從網際網路獲取資料,而不是使用難以模擬的靜態 http.get() 方法。

該函式現在應該如下所示:

dart
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())http.Client 提供給 fetchAlbum 方法。http.Client() 會建立一個預設的 http.Client

3. 使用模擬的 http.Client 建立測試檔案

#

接下來,建立一個測試檔案。

按照 單元測試簡介 指南中的建議,在根目錄的 test 資料夾中建立一個名為 fetch_album_test.dart 的檔案。

在主函式上新增 @GenerateMocks([], customMocks: [MockSpec<http.Client>(as: #MockHttpClient)]) 註解,以使用 mockito 生成 MockHttpClient 類。

生成的 MockHttpClient 類實現了 http.Client 類。這允許您將 MockHttpClient 傳遞給 fetchAlbum 函式,並在每個測試中返回不同的 HTTP 響應。

生成的模擬程式碼將位於 fetch_album_test.mocks.dart 中。匯入此檔案以使用它們。

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.
// Note: Naming the generated mock `MockHttpClient` to avoid confusion with
// `MockClient` from `package:http/testing.dart`.
@GenerateMocks([], customMocks: [MockSpec<http.Client>(as: #MockHttpClient)])
void main() {
}

接下來,執行以下命令來生成模擬:

dart run build_runner build

4. 為每種情況編寫測試

#

fetchAlbum() 函式執行以下兩種操作之一:

  1. 如果 HTTP 呼叫成功,則返回一個 Album
  2. 如果 HTTP 呼叫失敗,則丟擲一個 Exception

因此,您需要測試這兩種情況。使用 MockHttpClient 類在成功測試中返回“Ok”響應,在失敗測試中返回錯誤響應。使用 Mockito 提供的 when() 函式來測試這些條件。

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.
// Note: Naming the generated mock `MockHttpClient` to avoid confusion with
// `MockClient` from `package:http/testing.dart`.
@GenerateMocks([], customMocks: [MockSpec<http.Client>(as: #MockHttpClient)])
void main() {
  group('fetchAlbum', () {
    test('returns an Album if the http call completes successfully', () async {
      final client = MockHttpClient();

      // 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 = MockHttpClient();

      // 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
#
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
#
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.
// Note: Naming the generated mock `MockHttpClient` to avoid confusion with
// `MockClient` from `package:http/testing.dart`.
@GenerateMocks([], customMocks: [MockSpec<http.Client>(as: #MockHttpClient)])
void main() {
  group('fetchAlbum', () {
    test('returns an Album if the http call completes successfully', () async {
      final client = MockHttpClient();

      // 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 = MockHttpClient();

      // 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 來測試依賴於 Web 服務或資料庫的函式或類。這只是對 Mockito 庫和模擬概念的簡短介紹。有關更多資訊,請參閱 Mockito 軟體包 提供的文件。