Flutter Integration Test で WireMock を使用したモックサーバーと通信する
はじめに
Flutter Integration Test を実行するとき、 Web API との通信をどのようにするか迷っていました。
例えば、実際に Web API と通信するか、モックサーバーを用意して通信するか。
ひとつの答えとして、 WireMock を使用してモックサーバーを立てて通信する方法にしたので紹介します。
Flutter Integration Test で Web API との通信をどうするか
Flutter Integration Test で Web API との通信をどうするかは、以下のような方法が考えられます。
- 実際に Web API と通信する
- モックサーバーを用意して通信する
実際に Web API と通信するなら、 E2E テストとしての意味もあります。
ただ、その Web API が外部サービスである場合、テストが不安定になる可能性があります。
また、テストを実行する環境によって通信ができない場合もあります。
モックサーバーを用意して通信するなら、Web API の環境に関係なく Flutter の結合テストを実行できます。
そこで、今回はモックサーバーを用意し、Web API との通信する方法にしました。
モックサーバーに期待すること
モックサーバーに期待することは以下の通りです。
- 簡単に構築できる
- テストごとに指定したレスポンスを返せる
いくつかモックサーバーを調査し、 WireMock が期待することに合致していたので、 WireMock を使用することにしました。
WireMock とは
WireMock は、 HTTP モックサーバーとして動作する Java ライブラリです。
Java のスタンドアロンアプリケーションとして実行できます。
また、Docker イメージも提供されているので、 Docker コンテナとして実行もできます。
どのようなリクエストに対してどのようなレスポンスを返すかは、ファイルで設定できます。
また、 Administration API を使用してリクエストとレスポンスの設定もできます。
Flutter Integration Test で WireMock を使用する
ローカルで WireMock を起動
ローカルでの WireMock の起動は、Docker を利用することにしました。
compose.yml
ファイルを作成し、docker compose up
で起動します。
compose.yml
services:
wiremock:
image: wiremock/wiremock:3.9.2-1
volumes:
- ./wiremock/__files:/home/wiremock/__files
- ./wiremock/extensions:/var/wiremock/extensions
- ./wiremock/mappings:/home/wiremock/mappings
entrypoint:
[
"/docker-entrypoint.sh",
"--global-response-templating",
"--disable-gzip",
"--verbose",
]
ports:
- 8080:8080
Flutter Integration Test で WireMock Administration API を使用する準備
WireMock の Administration API を使用してリクエストとレスポンスの設定します。
以下のようなコードを書いて、 WireMock にリクエストとレスポンスの設定できるようにします。
(Web API の内容は、NewsAPI を例にしています)
integration_test/config/integration_test_config.dart
class IntegrationTestConfig {
factory IntegrationTestConfig() => instance;
IntegrationTestConfig._internal() {
wireMockBaseUrl = const String.fromEnvironment('wireMockBaseUrl');
}
static final IntegrationTestConfig instance =
IntegrationTestConfig._internal();
String wireMockBaseUrl = '';
}
integration_test/wiremock/wiremock_admin_mappings_client.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../config/integration_test_config.dart';
class WireMockAdminMappingsClient {
WireMockAdminMappingsClient();
String get _wireMockAdminMappingsUrl =>
'${IntegrationTestConfig.instance.wireMockBaseUrl}/__admin/mappings';
Future<http.Response> reset() async {
return http.post(
Uri.parse('$_wireMockAdminMappingsUrl/reset'),
);
}
Future<http.Response> createByJson(Map<String, dynamic> jsonData) async {
return http.post(
Uri.parse(_wireMockAdminMappingsUrl),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(jsonData),
);
}
}
integration_test/wiremock/newsapi/top_headlines/success.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../wiremock_admin_mappings_client.dart';
Future<http.Response> createNewsApiTopHeadlinesSuccessMockApi() {
return WireMockAdminMappingsClient().createByJson({
'request': {
'method': 'GET',
'urlPathPattern': '/newsapi/v2/top-headlines',
'queryParameters': {
'apiKey': {'matches': '.*'},
'category': {'matches': '.*'},
'pageSize': {'matches': '.*'},
'page': {'matches': '.*'},
},
},
'response': {
'status': 200,
'headers': {
'Content-Type': 'application/json',
},
'body': jsonEncode({
'status': 'ok',
'totalResults': 3,
'articles': [
{
'source': {
'id': 'source_1',
'name': 'source_name_1',
},
'title': 'title_1',
'description': 'description_1',
'url': 'https://example.com',
},
{
'source': {
'id': 'source_2',
'name': 'source_name_2',
},
'title': 'title_2',
'description': 'description_2',
'url': 'https://example.com',
},
{
'source': {
'id': 'source_3',
'name': 'source_name_3',
},
'title': 'title_3',
'description': 'description_3',
'url': 'https://example.com',
},
],
}),
},
});
}
integration_test/wiremock/newsapi/everything/success.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../wiremock_admin_mappings_client.dart';
Future<http.Response> createNewsApiEverythingSuccessMockApi() {
return WireMockAdminMappingsClient().createByJson({
'request': {
'method': 'GET',
'urlPathPattern': '/newsapi/v2/everything',
'queryParameters': {
'apiKey': {'matches': '.*'},
'q': {'matches': '.*'},
'pageSize': {'matches': '.*'},
'page': {'matches': '.*'},
},
},
'response': {
'status': 200,
'headers': {
'Content-Type': 'application/json',
},
'body': jsonEncode({
'status': 'ok',
'totalResults': 3,
'articles': [
{
'source': {
'id': 'source_1',
'name': 'source_name_1',
},
'title': 'title_1',
'description': 'description_1',
'url': 'https://example.com',
},
{
'source': {
'id': 'source_2',
'name': 'source_name_2',
},
'title': 'title_2',
'description': 'description_2',
'url': 'https://example.com',
},
{
'source': {
'id': 'source_3',
'name': 'source_name_3',
},
'title': 'title_3',
'description': 'description_3',
'url': 'https://example.com',
},
],
}),
},
});
}
Flutter Integration Test で WireMock を使用する
上記で WireMock の Administration API を使用してリクエストとレスポンスの設定する準備ができました。
次に、 Flutter Integration Test でテストコード内で WireMock を使用してリクエストとレスポンスの設定します。
integration_test/test/news_article_test.dart
import 'package:sample/main.dart' as app;
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../roboto/news_article_detail/news_article_detail_page_driver.dart';
import '../roboto/news_article_list/news_article_list_page_driver.dart';
import '../support/integration_test_widgets.dart';
import '../wiremock/newsapi/top_headlines/success.dart';
import '../wiremock/reset.dart';
void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
TestWidgets('テスト', binding, (tester) async {
await resetMockApi();
await createNewsApiTopHeadlinesSuccessMockApi();
await app.main();
// <省略>
});
}
Flutter Integration Test で WireMock を使用する注意点
Android エミュレーターで WireMock を使用する場合
WireMock はローカルで起動しているため、 Android エミュレーターからアクセスできるようにします。
アクセスするには localhost
ではなく、 10.0.2.2
を使用します。
iOS シミュレーターで WireMock を使用する場合
WireMock はローカルで起動しているため、iOS シミュレーターからアクセスできるようにします。
Android エミュレーターとは異なり、localhost
を使用します。
WireMock の接続先を設定
上記コードでは、 --dart-define
や --dart-define-from-file
を使用して、 WireMock の接続先を設定できるようにしています。
dart_define/integration_test_android.json
{
"wireMockBaseUrl": "http://10.0.2.2:8080"
}
dart_define/integration_test_ios.json
{
"wireMockBaseUrl": "http://localhost:8080"
}
Flutter Integration Test を実行
上記で WireMock を使用してリクエストとレスポンスの設定を含めたテストコードができました。
WireMock をが起動していることを確認してから、 Flutter Integration Test を実行します。
Android
flutter test --dart-define-from-file dart_define/integration_test_android.json integration_test/test/news_article_test.dart
iOS
flutter test --dart-define-from-file dart_define/integration_test_ios.json integration_test/test/news_article_test.dart
まとめ
Flutter Integration Test で WireMock を使用したモックサーバーと通信ができました。
Web API の環境に関係なく Flutter Integration Test をする方法として、 WireMock を使用してみてはいかがでしょうか。
Discussion