• トップ
  • ブログ一覧
  • 【Flutter】mock_web_serverを用いてAPIクライアントのテストを書いてみた
  • 【Flutter】mock_web_serverを用いてAPIクライアントのテストを書いてみた

    広告メディア事業部広告メディア事業部
    2022.08.05

    IT技術

    はじめに

    Retrofitを用いて実装したAPIクライアントのテストの書き方を調べる際にパッケージのリポジトリを漁っていたところ、mock_web_serverというパッケージを使用するとテスト内でモックサーバーを立てることができることがわかりました!

    そこで、今回はmock_web_serverを用いてRetrofitで実装したAPIクライアントのテストを書いてみます!

    retrofit の sampleテストコード

    開発環境

    開発環境は下記のとおりです。

    Flutter3.0.5

    使用パッケージ

    retrofit3.0.1+1
    mock_web_server5.0.0-nullsafety.1
    dio4.0.6
    json_annotation4.6.0
    build_runner2.2.0
    json_serializable6.3.1
    retrofit_generator4.0.2

    mock_web_serverを用いてAPIクライアントのテストを書く

    APIクライアントの実装

    それではテストの対象となるAPIクライアントを実装していきます。

    本記事の目的はテストを書くことにあるので、APIは架空のものとします。

    まずは使用パッケージをpubspec.ymlに追記します。

    1dependencies:
    2  flutter:
    3    sdk: flutter
    4  dio: ^4.0.6
    5  json_annotation: ^4.6.0
    6  retrofit: ^3.0.1+1
    7
    8dev_dependencies:
    9  flutter_test:
    10    sdk: flutter
    11  build_runner: ^2.2.0
    12  json_serializable: ^6.3.1
    13  retrofit_generator: ^4.0.2

    続いてモデル作成です。

    タスクを扱うモデルを作成します。(自動生成も同時に行います。)

    1import 'package:json_annotation/json_annotation.dart';
    2
    3part 'task.g.dart';
    4
    5@JsonSerializable()
    6class Task {
    7  const Task({
    8    required this.id,
    9    required this.name,
    10    required this.avatar,
    11    required this.createdAt,
    12  });
    13
    14  final String id;
    15  final String name;
    16  final String avatar;
    17  final String createdAt;
    18
    19  factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
    20
    21  Map<String, dynamic> toJson() => _$TaskToJson(this);
    22}

    次にAPI クライアントのクラスを作成します。(こちらも同様に自動生成を行います。)

    1import 'dart:io';
    2
    3import 'package:dio/dio.dart';
    4import 'package:mock_web_server_sample/model/group.dart';
    5import 'package:mock_web_server_sample/model/task.dart';
    6import 'package:retrofit/retrofit.dart';
    7
    8import '../request/fetch_group_request.dart';
    9
    10part 'rest_client.g.dart';
    11
    12abstract class RestClient {
    13  factory RestClient(Dio dio, {String baseUrl}) = _RestClient;
    14
    15  @GET('/tasks')
    16  Future<List<Task>> fetchTasks();
    17
    18  @POST("/tasks")
    19  Future<Task> createTask({
    20    @Body() required Task task,
    21  });
    22}

    テストの実装

    それでは上で実装したAPIクライアントのテストを書いていきます。

    まずはmock_web_serverをpubspec.ymlに追加します。

    1dev_dependencies:
    2  flutter_test:
    3    sdk: flutter
    4  build_runner: ^2.2.0
    5  json_serializable: ^6.3.1
    6  mock_web_server: ^5.0.0-nullsafety.1 # 追加
    7  retrofit_generator: ^4.0.2

    次にテストファイルを作成します。

    テストを書いていく前に、すべてのテストで使うことになるMockWebServerのインスタンスを返却するメソッドを追加します。

    1import 'dart:convert';
    2
    3import 'package:flutter_test/flutter_test.dart';
    4import 'package:mock_web_server/mock_web_server.dart';
    5
    6void main() {
    7  /// MockWebServerのインスタンスを生成
    8  Future<MockWebServer> _createMockWebServer({
    9    required Object mockResponse,
    10  }) async {
    11    final server = MockWebServer();
    12    addTearDown(server.shutdown);
    13    await server.start();
    14    // サーバー側からのレスポンスを定義
    15    server.enqueue(
    16      body: jsonEncode(mockResponse),
    17      headers: {'Content-Type': 'application/json'},
    18    );
    19    return server;
    20  }
    21}

    やっていることは下記のとおりです。

    1. MockWebServerのインスタンスを生成
    2. 各テスト終了時にMockWebServerを停止する処理を追加
    3. MockWebServerを起動
    4. server.enqueue でサーバー側からのレスポンスを定義(ここでHttpStatusCodeを設定することもできます)
    5. MockWebServerのインスタンスを返却

    ここで作成したインスタンスを用いることでモックサーバー向けにAPIを実行でき、リクエストやレスポンスの内容をテストできるようになります。

    それではテストを実際に書いていきます。
    確認事項は下記です。

    1. リクエストURLが期待どおりであること
    2. ヘッダーが期待どおりであること
    3. リクエストに含まれている情報が期待どおりであること

        1. リクエストURLが期待どおりであること

        リクエストURLはtakeRequest() で取得したリクエスト情報から.uri.path で取得できます。

        1    group('fetchTasks', () {
        2      test('リクエスト送信時 正しいエンドポイントにリクエストが送信されていること', () async {
        3        final server = await _createMockWebServer(mockResponse: [mockTask]);
        4        final restClient = RestClient(Dio(BaseOptions(baseUrl: server.url)));
        5        await restClient.fetchTasks();
        6        final target = server.takeRequest();
        7        expect(target.uri.path, '/tasks');
        8      });
        9    });

        2. ヘッダーが期待どおりであること

        ヘッダー情報はtakeRequest() で取得したリクエスト情報から.headers で取得できます。

        1    group('createTask', () {
        2      test('リクエスト送信時 正しいヘッダー情報が付与されていること', () async {
        3        final server = await _createMockWebServer(mockResponse: mockTask);
        4        final restClient = RestClient(Dio(BaseOptions(baseUrl: server.url)));
        5        await restClient.createTask(task: mockTask);
        6        final target = server.takeRequest();
        7        expect(
        8          target.headers[Headers.contentTypeHeader],
        9          Headers.jsonContentType,
        10        );
        11      });
        12    });

        3. リクエストに含まれている情報が期待どおりであること

        リクエストに含まれているbodyやqueryは、takeRequest() で取得したリクエスト情報から.body.query で取得できます。

        1    group('createTask', () {
        2      test('リクエスト送信時 リクエストに含まれている情報が正しいこと', () async {
        3        final server = await _createMockWebServer(mockResponse: mockTask);
        4        final restClient = RestClient(Dio(BaseOptions(baseUrl: server.url)));
        5        await restClient.createTask(task: mockTask);
        6        final target = server.takeRequest();
        7        expect(json.decode(target.body!), mockTask.toJson());
        8      });
        9    });

        まとめ

        これでAPIと実際に通信しなくても期待通りに動くことが担保できるようになりました!

        テストを書くことで実際に疎通せずとも実装ミスに気づけたりもするので、なるべく書いていきたいですね!

        ライトコードでは、エンジニアを積極採用中!

        ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

        採用情報へ

        広告メディア事業部

        広告メディア事業部

        おすすめ記事

        エンジニア大募集中!

        ライトコードでは、エンジニアを積極採用中です。

        特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

        また、フリーランスエンジニア様も大募集中です。

        background