はじめに
アプリケーション開発において、APIテスト大切なものであり、テストを行うことで、アプリケーションが外部のデータソースやサービスと正しく通信できることを保証できます。
http_mock_adapter
は、FlutterのDioライブラリ用のテストツールで、実際のAPIサーバーにリクエストを送ることなく、APIレスポンスを模倣(モック)してテストを行うことが可能です。これにより、実際のサーバーがなくても、APIの動作を効率的にテストできます。
この記事では、http_mock_adapter を使用してAPIのモックを作成し、基本的なテストコードの書き方について解説します。
環境
・Flutter 3.16.4
・パッケージは以下を使用(本記事で使用するもののみ書いています。)
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は映画の情報や評価、ジャンルなど様々なデータを取得できます。
プロジェクト構成
- api:TMDB APIとの通信を管理するクラスを格納
- models: APIから取得したデータの構造を定義するモデルクラスを格納
- tests:テスト関連のファイルを含むディレクトリ
- stubには、テスト用のモックデータ(例: movies_search.json)が格納されています。
- fixture.dart はテストデータを読み込むためのユーティリティ関数格納
- movies_search_test.dart はAPIのテストケースになります。
テストイメージ
TMDB APIを使用して映画データを取得することを想定しています。テストのために、以下のようなmovies_search.jsonというstub(テスト用の模擬データ)を用意しました。
{
"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から取得する映画データを表現するために、MoviesListData
とMoviesSearchData
の二つのモデルクラスを定義します。これらのクラスは、freezedライブラリを使用して作成します。
①MoviesListData
MoviesListData クラスは、個々の映画に関する情報(例えば、ID、タイトル、ポスターのパスなど)を保持します。
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 クラスは、映画検索の結果を表すために使用され、検索結果のリストやページ情報などを含みます。
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
というクラスのオブジェクトに変換します。
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
を定義します。
import 'dart:io';
String fixture(String name) {
return File('test/stub/$name').readAsStringSync();
}
②movies_search_test.dartの作成
http_mock_adapter
を使用してAPIのレスポンスをモックし、TmdbApiClient
クラスが期待通りに動作するかを検証します。
全コード
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
テストが全て正常に完了し、期待される結果が得られると以下のようになります。