LoginSignup
2
0

【Flutter】APIテストを簡単に!http_mock_adapterの使い方入門

Last updated at Posted at 2023-12-21

はじめに

アプリケーション開発において、APIテスト大切なものであり、テストを行うことで、アプリケーションが外部のデータソースやサービスと正しく通信できることを保証できます。

http_mock_adapterは、FlutterのDioライブラリ用のテストツールで、実際のAPIサーバーにリクエストを送ることなく、APIレスポンスを模倣(モック)してテストを行うことが可能です。これにより、実際のサーバーがなくても、APIの動作を効率的にテストできます。

この記事では、http_mock_adapter を使用してAPIのモックを作成し、基本的なテストコードの書き方について解説します。

環境

・Flutter 3.16.4
・パッケージは以下を使用(本記事で使用するもののみ書いています。)

pubspec.yaml
dependencies:
  freezed_annotation: ^2.4.1
  dio: ^5.4.0
  retrofit: ^4.0.3
  json_annotation: ^4.8.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.4.7
  freezed: ^2.4.6
  json_serializable: ^6.7.1
  retrofit_generator: ^8.0.6
  http_mock_adapter: ^0.6.1

今回のメインは以下になります。
・http_mock_adapter
・dio

※RetrofitやFreezedの使用方法等については本記事では深くは触れません。

使用するAPI

今回使用するAPIは、映画データベースを提供する人気のあるTMDB APIです。
このAPIは映画の情報や評価、ジャンルなど様々なデータを取得できます。

プロジェクト構成

※構成はあくまで一例です。
スクリーンショット 2023-12-21 15.15.14.png

  • api:TMDB APIとの通信を管理するクラスを格納
  • models: APIから取得したデータの構造を定義するモデルクラスを格納
  • tests:テスト関連のファイルを含むディレクトリ
    • stubには、テスト用のモックデータ(例: movies_search.json)が格納されています。
    • fixture.dart はテストデータを読み込むためのユーティリティ関数格納
    • movies_search_test.dart はAPIのテストケースになります。

テストイメージ

TMDB APIを使用して映画データを取得することを想定しています。テストのために、以下のようなmovies_search.jsonというstub(テスト用の模擬データ)を用意しました。

movies_search.json
{
  "page": 1,
  "results": [
    {
      "id": 1678,
      "original_language": "ja",
      "overview": "原水爆実験の影響で、大戸島の伝説の怪獣ゴジラが復活し、東京に上陸。帝都は蹂躙され廃墟と化した。ゴジラ抹殺の手段はあるのか・・・。戦後の日本映画界に特撮怪獣映画というジャンルを築いた、記念すべきゴジラ映画第1作。核の恐怖を描いた、本多猪四郎の真摯な本編ドラマと、円谷英二のリアリズム溢れる特撮演出が絶妙のコンビネーションを見せ、「ゴジラ」の名を一躍世界に轟かせた傑作。",
      "poster_path": "/lNxPgdQ0RThTVGVgE3j5mzaP6Ku.jpg",
      "title": "ゴジラ",
    },
  ],
	"total_pages": 4,
}

このデータは、TMDB APIのURL(https://api.themoviedb.org/3/)に、エンドポイント(search/movie)とクエリパラメータ(例:query=ゴジラ)を設定した際のレスポンスの例です(例なので実際のものから抜粋しています)。

通常、APIを使用する際にはapi_keyを設定する必要がありますが、今回はテストのためにモックデータを使用するので、api_keyは不要です。

実装手順

APIテストを行う前に、いくつかの準備が必要です。
まず、API通信を管理するクラスと、APIから取得したデータの構造を定義するクラスを作成します。

(1)データモデルの定義

TMDB APIから取得する映画データを表現するために、MoviesListDataMoviesSearchDataの二つのモデルクラスを定義します。これらのクラスは、freezedライブラリを使用して作成します。

①MoviesListData

MoviesListData クラスは、個々の映画に関する情報(例えば、ID、タイトル、ポスターのパスなど)を保持します。

movies_list_data.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'movies_list_data.freezed.dart';

part 'movies_list_data.g.dart';

@freezed
class MoviesListData with _$MoviesListData {
  const factory MoviesListData({
    int? id,
    String? title,
    @JsonKey(name: 'poster_path') String? posterPath,
  }) = _MoviesListData;

  factory MoviesListData.fromJson(Map<String, dynamic> json) =>
      _$MoviesListDataFromJson(json);

  const MoviesListData._();

  String get fullPosterPath => 'https://image.tmdb.org/t/p/w500/$posterPath';
}

②MoviesSearchData

MoviesSearchData クラスは、映画検索の結果を表すために使用され、検索結果のリストやページ情報などを含みます。

movies_search_data.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:mockito_sample/models/movies_search_data/movies_list_data.dart';

part 'movies_search_data.freezed.dart';

part 'movies_search_data.g.dart';

@freezed
class MoviesSearchData with _$MoviesSearchData {
  const factory MoviesSearchData({
    @Default(1) int page,
    @JsonKey(name: 'total_pages') @Default(1) int totalPages,
    @Default(<MoviesListData>[]) List<MoviesListData> results,
  }) = _MoviesSearchData;

  factory MoviesSearchData.fromJson(Map<String, dynamic> json) =>
      _$MoviesSearchDataFromJson(json);
}

(2)APIのリクエストとレスポンスの処理

TMDB APIの
APIのリクエストとレスポンスの処理には、RetrofitとDioを組み合わせて使用します。
Retrofitは、APIリクエストを簡単に定義し、実行するためのツールです。
Retrofitを使用することで、APIからデータを取得するコードをより簡潔かつ効率的に書くことができます。

TmdbApiClient

このクラスは、TMDB APIのsearch/movieエンドポイントに対するリクエストを処理し、検索結果をMoviesSearchDataというクラスのオブジェクトに変換します。

api_client.dart
import 'package:mockito_sample/models/movies_search_data/movies_search_data.dart';
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';

part 'api_client.g.dart';

@RestApi(baseUrl: 'https://api.themoviedb.org/3/')
abstract class TmdbApiClient {
  factory TmdbApiClient(Dio dio, {String baseUrl}) = _TmdbApiClient;

  @GET('search/movie')
  Future<MoviesSearchData> fetchSearchMoviesItems(
    @Query('query') String searchQuery, {
    @Query('page') int page = 1,
    @Query('language') String language = 'ja-JA',
  });
}

コードを生成は忘れないようにしましょう。

flutter pub run build_runner build --delete-conflicting-outputs

(3)テストコードの実装

いよいよ、TMDB APIのテストコードを実装します。
このプロセスでは、まずテストデータを読み込むためのユーティリティ関数を作成し、次に実際のテストケースを記述します。

①fixture.dartの準備

テストデータをファイルから読み込むために、fixture.dartというファイルを作成します。
このファイルには、テストデータファイル(stub)の内容を文字列として読み込むための関数fixtureを定義します。

fixture.dart
import 'dart:io';

String fixture(String name) {
  return File('test/stub/$name').readAsStringSync();
}

②movies_search_test.dartの作成

http_mock_adapterを使用してAPIのレスポンスをモックし、TmdbApiClientクラスが期待通りに動作するかを検証します。

全コード
movies_search_test.dart
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http_mock_adapter/http_mock_adapter.dart';
import 'package:mockito_sample/api/api_client.dart';
import 'package:mockito_sample/models/movies_search_data/movies_search_data.dart';

import 'fixture.dart';

void main() {
  group('TmdbApiClient Tests', () {
    late Dio dio;
    late DioAdapter dioAdapter;
    late TmdbApiClient apiClient;
    // 各テストケースが実行される前に行う初期設定
    setUp(() {
      dio = Dio();
      dioAdapter = DioAdapter(dio: dio);
      apiClient = TmdbApiClient(dio);
    });
    // 映画データを返すことをテスト
    test('fetchSearchMoviesItems returns MoviesSearchData for valid response',
        () async {
      // スタブデータ
      final mockResponse = fixture('movies_search.json');
      // 成功したレスポンスを返すように設定
      dioAdapter.onGet(
        'search/movie',
        (server) => server.reply(200, jsonDecode(mockResponse)),
        queryParameters: {
          'query': 'ゴジラ',
          'language': 'ja-JA',
          'page': 1,
        },
      );
      // APIクライアントを通じてデータを取得
      final result = await apiClient.fetchSearchMoviesItems('ゴジラ');
      // レスポンスが MoviesSearchData型であることを検証
      expect(result, isA<MoviesSearchData>());
    });

    test('fetchSearchMoviesItems throws error for 404 response', () async {
      // エラーレスポンスを返すように設定
      dioAdapter.onGet(
        'search/movie',
        (server) => server.reply(404, {'message': 'Not Found'}),
        queryParameters: {
          'query': 'モニラ',
          'language': 'ja-JA',
          'page': 1,
        },
      );
      // APIメソッドがエラーを投げることを検証
      expect(() async => await apiClient.fetchSearchMoviesItems('モニラ'),
          throwsA(isA<DioException>()));
    });
  });
}
①初期設定

各テストケースが実行される前に、Dio、DioAdapter、そしてTmdbApiClientクラスのインスタンスを作成します。

setUp(() {
  dio = Dio();
  dioAdapter = DioAdapter(dio: dio);
  apiClient = TmdbApiClient(dio);
});
②test

test関数を使用して個々のテストケースを定義します。第一引数にはテストケースの名前を記述し、第二引数にはテストを実行する関数を渡します。

test('fetchSearchMoviesItems returns MoviesSearchData for valid response', () async {
  
});
③スタブデータの使用
final mockResponse = fixture('movies_search.json');
④APIレスポンスのモック

dioAdapter.onGetメソッドを使用して、特定のAPIリクエスト(今回はsearch/movie)に対するレスポンスをモックします。
これにより、実際のAPIサーバーにアクセスすることなく、APIの動作をテストできます。

dioAdapter.onGet(
    'search/movie',
    (server) => server.reply(200, jsonDecode(mockResponse)),
    queryParameters: {
      'query': 'ゴジラ',
      'language': 'ja-JA',
      'page': 1,
    },
);

⑤テストケースの記述

今回は正常なレスポンスとエラーレスポンスの両方のシナリオに対するテストケースを記述してみました。

expect関数を使用して、APIクライアントが期待される結果(例えば、MoviesSearchData オブジェクト)を返すか、または適切なエラーを投げるかを検証します。

正常なレスポンスのテストケース例

final result = await apiClient.fetchSearchMoviesItems('ゴジラ');
// レスポンスが MoviesSearchData型であることを検証
expect(result, isA<MoviesSearchData>());

テストの実行と結果の確認

テストケースの記述が完了したら、実際にテストを実行して、すべてが期待通りに動作するかを確認します。
以下のコマンドを使用して、movies_search_test.dart ファイルに記述されたテストを実行できます。

flutter test test/movies_search_test.dart --reporter expanded

テストが全て正常に完了し、期待される結果が得られると以下のようになります。
スクリーンショット 2023-12-21 17.28.21.png

今回はAPIテストに特化しているため、全体的な開発プロセスについてはカバーしていませんが、実際の開発ではさまざまなテストケースを想定して書いていきたいです。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0