【Flutter】Interceptorを丁寧にテストするためのベストプラクティス

## はじめに

dioのinterceptorの実装方法については、以前こちらの記事で解説しました。

https://qiita.com/Harx02663971/items/4378d6521f804252625e

一方で、 `Interceptor` のテストを行うには少々工夫が必要です。

この記事では、**実践的なテスト手法とその背景にある考え方**を具体的に紹介します。
「どこまでテストすべきか」「何をどうモックすべきか」といった疑問を持った方の助けになれば幸いです。

### 記事の対象者

- Flutter で Dio を使ってネットワーク層を構築している方
- 自作の `Interceptor` をしっかりテストしたい方
- `dio_cache_interceptor` や `dio_smart_retry` などの拡張パッケージを導入している方
- `HttpClientAdapter` の仕組みに興味がある方
- 「モックで表面的なテストはできたけど、本当に動作確認したい」と思っている方

### 記事を執筆時点での筆者の環境

```
[✓] Flutter (Channel stable, 3.29.0, on macOS
    15.3.1 24D70 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android
    devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] VS Code (version 1.100.0)
```
### ソースコード

https://github.com/HaruhikoMotokawa/dio_interceptors_sample/tree/main

::: message
記事内では必要箇所のコードを抜粋していますが、全体を掴みたい方はGitHubをご覧ください。
:::

## テストの前提

今回は以下の条件であることを含みます。
- riverpodを使って依存性注入を行う
- mockitoを使ってモックを作成する

https://pub.dev/packages/riverpod

https://pub.dev/packages/mockito

## `Interceptor` のテストにおける基本方針

### handlerをモックする

`Interceptor` のテストを行う上で、基本的な考え方はそれぞれのhandlerを呼ばれているかどうかをテストします。
`Interceptor` を継承した独自のクラスの基本形は以下となります。

```dart
class ExampleInterceptor extends Interceptor {
  const ExampleInterceptor();

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    // このインターセプターで挟みたい処理を記述
    //...
    handler.next(options); // 次のInterceptorに進む
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    // このインターセプターで挟みたい処理を記述
    //...
    handler.next(response); // 次のInterceptorに進む
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    // このインターセプターで挟みたい処理を記述
    //...
    handler.next(err); // 次のInterceptorに進む
  }
}
```

よって、各メソッドの結果である `handler.next(...); ` を検証するために、まずは各種の `handler` をmockitoでモックします。

```dart:test/_mock/mock.dart
@GenerateNiceMocks([
  // ...
  MockSpec<RequestInterceptorHandler>(), // <----- 💡
  MockSpec<ResponseInterceptorHandler>(), // <----- 💡
  MockSpec<ErrorInterceptorHandler>(), // <----- 💡
  // ...
])
void main() {}
```

### `FakeErrorInterceptor` のテスト ~実装の確認　

手始めに `FakeErrorInterceptor` をテストしてみます。
この `Interceptor` はクラス内に保持したカウントに応じてエラーをスローします。
dioでのhttp通信を行うごとにカウントは増えていき、最終的には20回目でカウントをリセットします。

機能としては `onResponse` のみでしか反応しない作りになっています。
リクエストの送信やエラーが流れてきた場合はこのinterceptorは素通りします。

```dart:lib/data/sources/http/interceptors/_fake_error/fake_error_interceptor.dart
@Riverpod(keepAlive: true)
FakeErrorInterceptor fakeErrorInterceptor(Ref ref) => FakeErrorInterceptor();

class FakeErrorInterceptor extends Interceptor {
  int _count = 0;

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    final netWorkException = DioException(
      requestOptions: response.requestOptions,
      response: response,
      type: DioExceptionType.badCertificate,
    );

    final authException = DioException.badResponse(
      statusCode: 401,
      requestOptions: response.requestOptions,
      response: Response(
        requestOptions: response.requestOptions,
        statusCode: 401,
        data: response.data,
      ),
    );
    _count++;
    if (_count <= 2) {
      logger.w('FakeErrorInterceptor: Simulating error #$_count');
      // 第二位置引数のcallFollowingErrorInterceptorをtrueにすると
      // 次のInterceptorに進む
      handler.reject(netWorkException, true);
    } else if (_count == 11) {
      logger.w('FakeErrorInterceptor: Simulating error #$_count');
      handler.reject(authException, true);
    } else if (_count == 12) {
      logger.w('FakeErrorInterceptor: Simulating error #$_count');
      handler.reject(authException, true);
    } else if (_count == 20) {
      logger.i('FakeErrorInterceptor: Simulating count reset');
      _count = 0;
      handler.next(response);
    } else {
      // 正常であれば次のInterceptorに進む
      handler.next(response);
    }
  }
}
```

### テストコード ~ mainとsetUp

先に述べたように `FakeErrorInterceptor` では `handler` はレスポンスのみ実装されています。
よってmockitoで作成した `MockResponseInterceptorHandler` だけをモックで差し込んでいます。

```dart:lib/data/sources/http/interceptors/_fake_error/fake_error_interceptor.dart
void main() {
  late ProviderContainer container;
  late FakeErrorInterceptor interceptor;

  final mockResponseHandler = MockResponseInterceptorHandler();

  setUp(() {
    reset(mockResponseHandler);

    container = ProviderContainer();

    interceptor = container.read(fakeErrorInterceptorProvider);
  });

  tearDown(() {
    container.dispose();
  });

 // ...

}
```

### テストコード ~ テストの一例
ここでは一部のテストを抜粋してご紹介します。

```dart
  group('onResponse', () {
    test('レスポンスが１回目と２回目の時に DioException をスローする', () async {
      final response = Response(
        requestOptions: RequestOptions(path: '/test_api/test'),
        data: 'test',
      );

      // 1回目のレスポンスで DioException がスローされることを確認
      interceptor.onResponse(response, mockResponseHandler);
      verify(
        mockResponseHandler.reject(
          argThat(isA<DioException>()),
          true,
        ),
      ).called(1);

      // 2回目のレスポンスで DioException がスローされることを確認
      interceptor.onResponse(response, mockResponseHandler);
      verify(
        mockResponseHandler.reject(
          argThat(isA<DioException>()),
          true,
        ),
      ).called(1);

      // 3回目のレスポンスで正常に処理されることを確認
      interceptor.onResponse(response, mockResponseHandler);
      verify(mockResponseHandler.next(response)).called(1);
    });

    // ...
  });
```
テスト上で `interceptor.onResponse` を実行するためにモックした `handler` である`mockResponseHandler` を渡しています。
```dart
interceptor.onResponse(response, mockResponseHandler);
```

そしてレスポンスが流れてきた回数毎に `handler` が何を実行しているのかをmockitoの `verify` 関数で検証することができます。

このようにして `handler` をモックすればその他の `onError` や `onRequest` をテストすることも可能です。

## `DioCacheInterceptor` を使っている場合のテスト

キャッシュ機構を簡単に実装できるdio_cache_interceptorを実装した場合をのテストを見ていきます。

https://pub.dev/packages/dio_cache_interceptor

### テストしやすいように実装する

まず、テストしやすいように `DioCacheInterceptor` をクラス内で直接インスタンス化せずに `ref` 経由で渡す形にしています。

```dart:lib/data/sources/http/interceptors/cache/dio_cache_interceptor.dart
@Riverpod(keepAlive: true)
DioCacheInterceptor dioCacheInterceptor(Ref ref) {
  return DioCacheInterceptor(
    options: CacheOptions(
      store: MemCacheStore(),
      maxStale: const Duration(minutes: 10),
    ),
  );
}
```

```dart:lib/data/sources/http/interceptors/cache/custom_dio_cache_interceptor.dart
class CustomDioCacheInterceptor extends Interceptor {
  CustomDioCacheInterceptor(this.ref);

  final Ref ref;

  // 💡 ここでref経由でインスタンスを取得する
  DioCacheInterceptor get _interceptor => ref.read(dioCacheInterceptorProvider);

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    _interceptor.onRequest(options, handler);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    _interceptor.onResponse(response, handler);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    _interceptor.onError(err, handler);
  }
}
```
さらに `CustomDioCacheInterceptor` 自体もテストしやすいように provider化しておきます。

```dart:lib/data/sources/http/interceptors/cache/custom_dio_cache_interceptor.dart
@Riverpod(keepAlive: true)
CustomDioCacheInterceptor customDioCacheInterceptor(Ref ref) =>
    CustomDioCacheInterceptor(ref);
```

### テストコード ~mainとsetUp

今回の場合は `handler` は３種類全て検証するので、３種モックで差し込みます。
また、`DioCacheInterceptor` をモックで差し込みます。


```dart:test/_mock/mock.dart
@GenerateNiceMocks([
  MockSpec<DioCacheInterceptor>(), // <----- 💡
  MockSpec<RequestInterceptorHandler>(), // <----- 💡
  MockSpec<ResponseInterceptorHandler>(), // <----- 💡
  MockSpec<ErrorInterceptorHandler>(), // <----- 💡
  // ...
])
void main() {}
```



ここは `ref` 経由なので `overrideWithValue` を忘れずに行いましょう。
```dart:test/data/sources/http/interceptors/cache/custom_dio_cache_interceptor_test.dart
void main() {
  late ProviderContainer container;
  late CustomDioCacheInterceptor interceptor;

  final mockDioCacheInterceptor = MockDioCacheInterceptor();
  final mockRequestHandler = MockRequestInterceptorHandler();
  final mockResponseHandler = MockResponseInterceptorHandler();
  final mockErrorHandler = MockErrorInterceptorHandler();

  setUp(() {
    reset(mockDioCacheInterceptor);
    reset(mockRequestHandler);
    reset(mockResponseHandler);
    reset(mockErrorHandler);

    container = ProviderContainer(
      overrides: [
        dioCacheInterceptorProvider.overrideWithValue(mockDioCacheInterceptor),
      ],
    );

    interceptor = container.read(customDioCacheInterceptorProvider);
  });

  tearDown(() {
    container.dispose();
  });

  // ...
}
```

### テストコード ~onRequest

検証内容は以下のコメントにもあるとおり、`DioCacheInterceptor` の `onRequest` が呼ばれたかどうかです。
また、通常の `handler.next` が呼ばれていないことを `verifyNever` で検証します。
```dart:test/data/sources/http/interceptors/cache/custom_dio_cache_interceptor_test.dart
  group('onRequest', () {
    test('DioCacheInterceptor のonRequestが呼ばれる', () async {
      final requestOptions = RequestOptions(path: '/test_api/test');

      // リクエストを実行
      interceptor.onRequest(requestOptions, mockRequestHandler);

      // DioCacheInterceptor の onRequest が呼ばれたことを確認
      verify(
        mockDioCacheInterceptor.onRequest(
          requestOptions,
          mockRequestHandler,
        ),
      ).called(1);

      // 通常のリクエストハンドラーが呼ばれないことを確認
      verifyNever(mockResponseHandler.next(any));
    });
  });
```

その他の `onResponse` `onError` も同様なので、ここでは割愛します。

### キャッシュできているかをテストするには？ ~HttpClientAdapterを活用

前述したテスト内容だと `CustomDioCacheInterceptor` は内部で `DioCacheInterceptor` を使っているか？
ということしかテストしておらず、実際にキャッシュ機能が動いているかのテストにはなっていません。

そこでしっかりテストするにはhttp通信を複数回行った結果、dioの `fetch` 関数が１回しか呼ばれていないことを検証する必要があります。
ただ、そのままテスト内で実際のhttp通信を行うのはテストとしてよろしくありません。

通信環境に依存してしまう、本番のAPIと実際に通信してしまうので本番のAPI側の状態にテストが依存してしまいます。

そこでhttp通信をモックする必要があるのですが、そのモックを行うには `HttpClientAdapter` というものを実装したカスタムクラスを用意する必要があります。

https://pub.dev/documentation/dio/latest/dio/HttpClientAdapter-class.html

dioの内部処理の順番としては最初にinterceptorを通り、最後に `HttpClientAdapter` を通って通信を行っています。
基本的には `HttpClientAdapter` をカスタマイズすることはあまりないのですが、今回のようにテストで検証するときにはカスタムしたものを差し込むことで検証ができます。

:::message
有名なhttpモックパッケージのhttp_mock_adapterというのがありますが、これは内部でHttpClientAdapterを使っています。

https://pub.dev/packages/http_mock_adapter

ただ、今回のようにfetchの回数を数える機構がないので今回は自作します。
:::

### dioのfetchを監視するカスタム HttpClientAdapter

`HttpClientAdapter` を実装したカスタムクラスを作成します。

その中でfetch関数をオーバーライドして、その中に回数を数える機構を入れ込みます。

後に紹介するリトライ処理でも使えるように引数 `final DioException? dioException;` がある場合は例外をスローするようにしています。

また、今回はキャッシュができることを検証したいので、このレスポンスがキャッシュできることを許可するようにheadersに設定する必要があります。

```dart:test/test_util/fetch_behavior_test_adapter.dart
class FetchBehaviorTestAdapter implements HttpClientAdapter {
  FetchBehaviorTestAdapter({
    required this.onAttempt,
    this.responseData,
    this.dioException,
  });

  final void Function(int attempt) onAttempt;
  final String? responseData;
  final DioException? dioException;

  int _internalAttempt = 0;

  // 特に必要ないが、HttpClientAdapterのインターフェースに合わせるために実装
  @override
  void close({bool force = false}) {}

  @override
  Future<ResponseBody> fetch(
    RequestOptions options,
    Stream<List<int>>? requestStream,
    Future<void>? cancelFuture,
  ) async {
    _internalAttempt++; // 💡 <= ここでカウントを増やす
    onAttempt(_internalAttempt); // 💡 <= 外部で取得できるように関数で公開

    if (dioException case final dioException?) {
      throw dioException;
    } else {
      // レスポンスを返す
      return ResponseBody.fromString(
        responseData ?? 'success',
        200,
        headers: {
          Headers.contentTypeHeader: ['application/json'],
          'cache-control': ['public, max-age=120'], // 💡 <= ここが重要！ここでキャッシュを許可
        },
      );
    }
  }
}
```

### 独自のHttpClientAdapterを使ってキャッシュしているかをテストする

```dart:test/data/sources/http/interceptors/cache/custom_dio_cache_interceptor_test.dart
group('キャッシュの挙動確認', () {
  /// 全体テストとは別のcontainerを使う
  late ProviderContainer container;
  late CustomDioCacheInterceptor customDioCacheInterceptor;
  late Dio dio;
  late FetchBehaviorTestAdapter dioHttpClientAdapter;
  late int fetchCount;

  const path = '/test_api/test';
  const mockedResponse = 'mocked response';

  setUp(() {
    container = ProviderContainer();
    fetchCount = 0;
    dio = Dio(BaseOptions(baseUrl: 'https://example.com'));

    dioHttpClientAdapter = FetchBehaviorTestAdapter(
      onAttempt: (attempt) {
        fetchCount = attempt;
      },
      responseData: mockedResponse,
    );

    customDioCacheInterceptor =
        container.read(customDioCacheInterceptorProvider);

    // customDioCacheInterceptor をDioに追加
    dio.interceptors.add(customDioCacheInterceptor);

    // FetchBehaviorTestAdapterをDioのHttpClientAdapterに設定
    dio.httpClientAdapter = dioHttpClientAdapter;
  });

  tearDown(() {
    container.dispose();
  });

  test('同じリクエストはキャッシュが使われ fetch されない', () async {
    // 初回
    final res1 = await dio.get<String>(path);
    expect(res1.data, mockedResponse);

    // 2回目
    final res2 = await dio.get<String>(path);
    expect(res2.data, mockedResponse);

    // キャッシュを使っているので fetchCount は 1 のまま
    expect(fetchCount, equals(1));
  });
  test('異なるリクエストはキャッシュが使われず fetch される', () async {
    // 初回
    final res1 = await dio.get<String>(path);
    expect(res1.data, mockedResponse);

    // 異なるリクエスト
    final res2 = await dio.get<String>('/test_api/another_test');
    // 異なるリクエストでもレスポンスは同じになるように今回はモックされている
    expect(res2.data, mockedResponse);

    // 異なるリクエストなので fetchCount は 2 になる
    expect(fetchCount, equals(2));
  });
});
```

## `RetryInterceptor` を使っている場合のテスト

ここではリトライ処理を簡単に実装できるdio_smart_retryを実装した場合のテストを見ていきましょう。

https://pub.dev/packages/dio_smart_retry

基本的な流れは `DioCacheInterceptor` の時と同じで、依存性注入をriverpodで行うようにproviderで定義しています。
ここではリトライのテスト独自の部分について解説していきます。

### リトライする時間を差し替え可能にしておく

`RetryInterceptor` の設定項目のうち、`retryDelays` があります。
これはリトライから次のリトライを実行するまでの間隔をどの程度にするかの設定です。
基本設定はリトライの上限が３となっていることを前提としていますが、次のとおりです。

```dart
List<Duration> retryDelays = const [
  Duration(seconds: 1),
  Duration(seconds: 3),
  Duration(seconds: 5),
  ]
```
特に設定を変えなくてもいい場合はこのままとするのですが、このままだと実際にテストをした場合も同様の時間経過を待たなくてはなりません。
そこで差し替え可能なように定数化したものを設定します。

```dart:lib/data/sources/http/interceptors/retry/retry_interceptor.dart
@Riverpod(keepAlive: true)
RetryInterceptor retryInterceptor(
  Ref ref,
  Dio dio, {
  FutureOr<bool> Function(DioException, int)? retryEvaluator,
}) {
  return RetryInterceptor(
    dio: dio,
    retryEvaluator: retryEvaluator,
    logPrint: logger.d,
    retryDelays: Constants.dioRetryDelays, // 💡 <=　差し替え可能な変数を入れておく
  );
}
```
`Constants.dioRetryDelays` は次のとおりに宣言しています。
コメントにもありますが、ここでの宣言方法が `static` のみであり、`static const` ではないことが重要です。

```dart:lib/core/constant/constants.dart
abstract final class Constants {
  /// dioのリトライ待機時間
  ///
  /// テストで時間を変更できるようにするためstaticのみで宣言する
  static List<Duration> dioRetryDelays = const [
    Duration(seconds: 1),
    Duration(seconds: 3),
    Duration(seconds: 5),
  ];
}
```

次にtestディレクトリの配下に `flutter_test_config.dart` というファイルを作り以下のように書きます。
すると、このテストディレクトリ内では `Constants.dioRetryDelays` はここで設定した時間と差し替えることができ、実際の実装に影響を与えずに変更することができます。

つまり、このファイル内で書かれていることが実際のテストを行う前に一回だけ実行されるということですね。
例えるならテスト全体のsetUpAll関数といった感じでしょうか。

https://api.flutter.dev/flutter/flutter_test/

```dart:test/flutter_test_config.dart
/// テスト実行前に一番最初に実行される
///
/// ここで細かい設定を行える
///
/// https://api.flutter.dev/flutter/flutter_test/
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
  /// テストするときにリトライの時間をすべて0にする
  Constants.dioRetryDelays = [
    Duration.zero,
    Duration.zero,
    Duration.zero,
  ];

  await testMain();
}
```

### `RetryInterceptor` の評価関数の受け渡し

`RetryInterceptor` にはその関数がどのエラーに反応してリトライ処理を実行するのかの条件を関数で定義するようになっています。
しかし、テストでモックを差し込むときにも全く同じ関数を渡さないといけません。
わざわざテスト側で同じ関数を定義しなくてもいいようにこの関数は　`static`　で宣言します。
ただ、静的関数にしてしまうと間違った場所で呼ばれてしまう危険性もあるので、`@visibleForTesting`アノテーションをつけて警告が出るようにしておきましょう、

```dart:lib/data/sources/http/interceptors/retry/auth_error_retry_interceptor.dart
class AuthErrorRetryInterceptor extends Interceptor {
  AuthErrorRetryInterceptor(this.ref, this.dio);

  final Ref ref;
  final Dio dio;

  RetryInterceptor get _interceptor =>
      ref.read(retryInterceptorProvider(dio, retryEvaluator: retryEvaluator));

  /// 認証エラーのリトライを行うかどうかの評価関数
  ///
  /// AuthErrorRetryInterceptor内かテストのみで使用する
  @visibleForTesting
  static bool retryEvaluator(DioException error, int attempt) {
    if (error.type == DioExceptionType.badResponse &&
        error.response?.statusCode == 401) {
      return true;
    }

    return false;
  }

  // ...
}
```

テストでは以下のように `retryEvaluator` を渡しています。

```dart:test/data/sources/http/interceptors/retry/auth_error_retry_interceptor_test.dart
void main() {
  late ProviderContainer container;
  late AuthErrorRetryInterceptor interceptor;

  // ...
  final mockDio = MockDio();

  setUp(() {
    // ...
    reset(mockDio);

    container = ProviderContainer(
      overrides: [
        retryInterceptorProvider(
          mockDio,
          // 💡💡💡💡💡💡
          retryEvaluator: AuthErrorRetryInterceptor.retryEvaluator,
        ).overrideWithValue(mockRetryInterceptor),
      ],
    );

    interceptor = container.read(authErrorRetryInterceptorProvider(mockDio));
  });

  // ...
}
```

### 特定のエラーでリトライするかの挙動テスト
`DioCacheInterceptor` のテストの時と同様に独自の `HttpClientAdapter` である `FetchBehaviorTestAdapter` を使って検証しています。

差分としてはテストごとにスローするエラーが違うので、テストごとに `FetchBehaviorTestAdapter` をインスタンス化してセットしている点です。

```dart:test/data/sources/http/interceptors/retry/auth_error_retry_interceptor_test.dart
group('リトライの挙動テスト', () {
  /// 全体テストとは別のcontainerを使う
  late ProviderContainer container;
  late AuthErrorRetryInterceptor authErrorRetryInterceptor;
  late Dio dio;
  late int fetchCount;

  const path = '/test_api/test';
  setUp(() {
    container = ProviderContainer();
    fetchCount = 0;

    dio = Dio(BaseOptions(baseUrl: 'https://example.com'));
    authErrorRetryInterceptor =
        container.read(authErrorRetryInterceptorProvider(dio));

    dio.interceptors.add(authErrorRetryInterceptor);
  });

  tearDown(() {
    container.dispose();
  });

  test('DioExceptionType.badResponse の場合、3回リトライされる', () async {
    // 検証用のHttpClientAdapterを作成
    final adapter = FetchBehaviorTestAdapter(
      onAttempt: (attempt) {
        fetchCount = attempt;
      },
      dioException: DioException(
        requestOptions: RequestOptions(path: path),
        type: DioExceptionType.badResponse,
        response: Response(
          requestOptions: RequestOptions(path: path),
          statusCode: 401,
        ),
      ),
    );

    // Dioにアダプターを設定
    dio.httpClientAdapter = adapter;

    // 処理を実行し、最終的にDioExceptionがスローされることを確認
    await expectLater(
      dio.get<String>(path),
      throwsA(
        isA<DioException>()
            .having((e) => e.response?.statusCode, 'statusCode', 401)
            .having(
              (e) => e.type,
              'type',
              DioExceptionType.badResponse,
            ),
      ),
    );

    // 初回１ + リトライ3回 = ４回行われている
    expect(fetchCount, equals(4));
  });
  test('DioExceptionType.badResponse以外 の場合は何もしない', () async {
    // 検証用のHttpClientAdapterを作成
    final adapter = FetchBehaviorTestAdapter(
      onAttempt: (attempt) {
        fetchCount = attempt;
      },
      dioException: DioException(
        requestOptions: RequestOptions(path: path),
        type: DioExceptionType.connectionTimeout,
        response: Response(
          requestOptions: RequestOptions(path: path),
        ),
      ),
    );

    // Dioにアダプターを設定
    dio.httpClientAdapter = adapter;

    // 処理を実行し、最終的にDioExceptionがスローされることを確認
    await expectLater(
      dio.get<String>(path),
      throwsA(
        isA<DioException>().having(
          (e) => e.type,
          'type',
          DioExceptionType.connectionTimeout,
        ),
      ),
    );

    // リトライは行われないので、1回だけ呼ばれる
    expect(fetchCount, equals(1));
  });
});
```

::: message
`onRequest` , `onResponse` , `onError` のテストの掲載は割愛します。
:::

## 終わりに

`Interceptor` のテストは一見地味ですが、アプリの信頼性を高めるうえで非常に重要な工程です。

今回紹介したように、handler のモックやカスタム `HttpClientAdapter` の活用により、`Interceptor` の挙動を精密に検証できるようになります。
とくにキャッシュやリトライといった仕組みは、正しく動作しているかどうかの確認が難しい分、こうしたアプローチが有効です。

この記事が `Interceptor` テストの第一歩、あるいは次の改善ステップの参考になれば幸いです。