🙆

【Flutter】Mockito を使用したテストを学ぶ

に公開2

https://docs.flutter.dev/cookbook/testing/unit/mocking

はじめに

テストは品質を保証するために欠かせません。特に外部サービスやデータベースに依存するコードをテストする場合、実際のサービスを使うと以下の問題が生じます:

  • テストの実行速度が遅くなる
  • 外部サービスの応答によってテスト結果が変わる(不安定なテスト)
  • すべての成功・失敗パターンをテストすることが難しい

このような問題を解決するために「モック」と呼ばれる手法があります。この記事では、「Mockito」パッケージを使って、依存関係をモック化する方法をメモとして記しておきます。

サンプルコード

https://github.com/muranakar/mockito_sample

モック化とは?

モック化とは、実際のクラスや関数の代わりに使用する「ニセモノ」を作成する技術です。このニセモノは本物のように振る舞いますが、テストに都合の良い結果を返すように設定できます。

手順

Mockito を使った依存関係のモック化は、以下の手順で行います:

  1. パッケージの追加
  2. テスト対象の関数を作成
  3. モックを使用したテストファイルの作成
  4. 各条件に対するテストの実装
  5. テストの実行

それでは、具体的に見ていきましょう。

1. パッケージの追加

まず、必要なパッケージをpubspec.yamlファイルに追加します。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0

  cupertino_icons: ^1.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^5.4.0
  analyzer: ^7.4.1
  build_runner: ^2.4.0
  flutter_lints: ^5.0.0

dependency_overrides:
  analyzer: 7.3.0

https://github.com/dart-lang/sdk/issues/60545

2. テスト対象の関数を作成

今回は、HTTP クライアントを使用してウェブサービスからデータを取得する関数をテスト対象とします。

main.dart
// アルバムデータのモデルクラス
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,
    );
  }
}

// アルバム情報を取得する関数
Future<Album> fetchAlbum(http.Client client) async {
  // HTTPクライアントを使ってAPIからデータを取得
  final response = await client.get(
    Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
  );

  // レスポンスが成功の場合、JSONをパースしてAlbumオブジェクトを返す
  if (response.statusCode == 200) {
    return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
  } else {
    // 失敗の場合は例外をスロー
    throw Exception('Failed to load album');
  }
}

この関数には 2 つの特徴があります:

  1. http.Clientを引数として受け取る
  2. 受け取ったクライアントを使って HTTP リクエストを行う

この設計により、テスト時に本物の HTTP クライアントの代わりにモッククライアントを渡すことができます。

3. モックを使用したテストファイルの作成

次に、テストファイルを作成します。ファイル名はfetch_album_test.dartとし、testフォルダに配置します。

fetch_album_test.dart
// http.Clientのモッククラスを生成するアノテーション
@GenerateMocks([http.Client])
void main() {
  // テストはここに実装します
}

@GenerateMocks([http.Client])というアノテーションは、http.Clientクラスのモックを自動生成するための指示です。

モッククラスを生成するために、以下のコマンドを実行します:

dart run build_runner build

このコマンドを実行すると、fetch_album_test.mocks.dartというファイルが生成されます。このファイルには、MockClientクラスが含まれています。

4. 各条件に対するテストの実装

fetchAlbum()関数は以下の 2 つの状況が考えられます:

  1. HTTP リクエストが成功した場合 → Album オブジェクトを返す
  2. HTTP リクエストが失敗した場合 → 例外をスロー

それぞれの状況に対するテストを実装していきます。

fetch_album_test.dart

@GenerateMocks([http.Client])
void main() {
  group('fetchAlbum', () {
    // 成功ケースのテスト
    test('HTTPリクエストが成功した場合、Albumオブジェクトを返すこと', () async {
      // モッククライアントを作成
      final client = MockClient();

      // モッククライアントの振る舞いを設定
      // 特定のURLへのリクエストに対して、成功レスポンスを返すように指定
      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('HTTPリクエストが失敗した場合、例外がスローされること', () {
      // モッククライアントを作成
      final client = MockClient();

      // モッククライアントの振る舞いを設定
      // 特定のURLへのリクエストに対して、エラーレスポンスを返すように指定
      when(
        client.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')),
      ).thenAnswer((_) async => http.Response('Not Found', 404));

      // テスト対象の関数を実行し、例外がスローされることを検証
      expect(fetchAlbum(client), throwsException);
    });
  });
}

この実装では、Mockito のwhen()thenAnswer()メソッドを使用して、モッククライアントの振る舞いを定義しています。

  • 成功ケース:ステータスコード 200 と有効な JSON を含むレスポンスを返す
  • 失敗ケース:ステータスコード 404 とエラーメッセージを含むレスポンスを返す

5. テストの実行

テストを実行するには、以下のコマンドを使用します:

flutter test test/fetch_album_test.dart

テストが成功すると、以下のような出力が表示されます:

モック化のメリット

モック化には以下のようなメリットがあります:

  1. テストの速度向上:実際のネットワーク通信やデータベースアクセスを行わないため、テストが高速に実行できます。

  2. テストの安定性向上:外部サービスの状態に依存せず、常に同じ結果を返すため、テスト結果が安定します。

  3. すべての条件をテスト可能:実際のサービスでは再現が難しいエラーケースなども、モックを使えば簡単にテストできます。

Discussion

はるさんMobile.Junior.EngineerはるさんMobile.Junior.Engineer

モックのアノテーションですが、公式には'@GenerateNiceMocks'を使うように推奨していますので、ご確認ください!

https://pub.dev/packages/mockito