📑

Flutter Integration Test で WireMock を使用したモックサーバーと通信する

2024/11/20に公開

はじめに

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 とは

https://wiremock.org/

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