🔨

dio & mockitoでテストコードを書く

2025/03/06に公開

ライブラリを使用してテストを書く

dioだと専用のライブラリを使用するとテストコードを書きやすくなるのだが、どうやらもうメンテナンスされていないようだ🤔

https://pub.dev/packages/http_mock_adapter/example

なので案件でも使用することのあったmockitoを使ってみた。

https://pub.dev/packages/mockito
https://pub.dev/packages/dio
https://pub.dev/packages/build_runner

pubspec.yamlにモジュールを追加してみよう。

name: fvm_dio_mockito
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0

environment:
  sdk: ^3.7.0

dependencies:
  flutter:
    sdk: flutter
  dio: ^5.8.0+1
  
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0
  mockito: ^5.4.5
  build_runner: ^2.4.15

flutter:
  uses-material-design: true

テストコードのファイルに全て書いてしまったが、仕事だと分けることがある。データソースのクラスを作り、リポジトリを作り本番用とテスト用のクラスを分けている。

そもそもテストコードをなぜ書くのか?
正しくメソッドが実行されているかみたいというのもあるが、まだAPIが作られていないことがある。そのようなケースでは、テストコードを書いてモックとスタブを使用してテストをする。

ライブラリを使わないこともある。その場合は引数を渡すだけのテストコードを書く。

test/user_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:dio/dio.dart';

import 'login_test.mocks.dart';

// サンプルのクラス
class UserService {
  final Dio dio;

  UserService(this.dio);

  Future<Map<String, dynamic>> getUser(int id) async {
    final response = await dio.get('/users/$id');
    return response.data;
  }

  Future<void> createUser(String name, String email) async {
    await dio.post('/users', data: {'name': name, 'email': email});
  }
}

// モックの生成
([Dio])
void main() {
  late MockDio mockDio;
  late UserService userService;

  setUp(() {
    mockDio = MockDio();
    userService = UserService(mockDio);
  });

  group('UserService Tests', () {
    // スタブ (when-thenReturn/thenAnswer) の例
    test('getUser - スタブの使用例', () async {
      // 特定の入力に対する戻り値を定義する - これがスタブ
      when(mockDio.get('/users/1')).thenAnswer(
        (_) async => Response(
          data: {'id': 1, 'name': 'Test User', 'email': 'test@example.com'},
          statusCode: 200,
          requestOptions: RequestOptions(path: '/users/1'),
        ),
      );

      final result = await userService.getUser(1);

      expect(result['name'], 'Test User');
      expect(result['email'], 'test@example.com');

      // スタブしただけでは、メソッドが呼ばれたことを検証していない
    });

    // モック (verify) の例
    test('createUser - モック検証の使用例', () async {
      // 動作をスタブする
      when(mockDio.post(any, data: anyNamed('data'))).thenAnswer(
        (_) async => Response(
          data: {'success': true},
          statusCode: 201,
          requestOptions: RequestOptions(path: '/users'),
        ),
      );

      // メソッド実行
      await userService.createUser('New User', 'new@example.com');

      // メソッドが特定のパラメータで呼ばれたことを検証する - これがモック
      verify(
        mockDio.post(
          '/users',
          data: {'name': 'New User', 'email': 'new@example.com'},
        ),
      ).called(1);
    });

    // モックとスタブの両方を使用する例
    test('完全なテスト - スタブとモック検証の両方を使用', () async {
      // 1. スタブを設定
      when(mockDio.get('/users/2')).thenAnswer(
        (_) async => Response(
          data: {
            'id': 2,
            'name': 'Another User',
            'email': 'another@example.com',
          },
          statusCode: 200,
          requestOptions: RequestOptions(path: '/users/2'),
        ),
      );

      // 2. テスト対象メソッドを実行
      final result = await userService.getUser(2);

      // 3. 結果を検証
      expect(result['name'], 'Another User');

      // 4. モック検証 - メソッドが正しく呼ばれたことを確認
      verify(mockDio.get('/users/2')).called(1);
    });

    // 例外をスローするスタブの例
    test('例外をスローするスタブ', () async {
      // エラーをスローするスタブを設定
      when(mockDio.get('/users/999')).thenThrow(
        DioException(
          requestOptions: RequestOptions(path: '/users/999'),
          error: 'Not Found',
          response: Response(
            statusCode: 404,
            requestOptions: RequestOptions(path: '/users/999'),
          ),
        ),
      );

      // 例外が発生することを期待
      expect(() => userService.getUser(999), throwsA(isA<DioException>()));
    });

    // 任意のパラメータに対するスタブ設定
    test('任意のパラメータに対するスタブ設定', () async {
      // どんなパスでもOKなスタブ
      when(mockDio.get(any)).thenAnswer(
        (_) async => Response(
          data: {'id': 0, 'name': 'Generic Response'},
          statusCode: 200,
          requestOptions: RequestOptions(path: ''),
        ),
      );

      final result = await userService.getUser(42);
      expect(result['name'], 'Generic Response');
    });
  });
}

テストコードを定義したらmockitoを使用するので自動生成のコマンドを実行する。

fvm使っていたのでコマンドの先頭につけてます。

fvm flutter pub run build_runner watch --delete-conflicting-outputs

テストコードを実行してみる。

fvm flutter test test/user_service_test.dart 

RunやDebugボタンを押してもテストは実行できる。

Mockitoでモック(mock)とスタブ(stub)を使い分ける方法について説明します。

Mockitoでモックとスタブを使い分ける際の主なポイントは次のとおりです:

まとめ

モック(Mock)とスタブ(Stub)の違い

  1. スタブ:

    • 特定のメソッド呼び出しに対して「何を返すか」を定義します
    • when(...).thenReturn(...)when(...).thenAnswer(...) を使います
    • スタブはテスト対象のメソッドに入力を提供します
  2. モック検証:

    • メソッドが「呼び出されたかどうか」と「どのように呼び出されたか」を検証します
    • verify(...) を使用します
    • モック検証はテスト対象のメソッドが期待通りの呼び出しを行ったか確認します

使い分けのシナリオ

  • 入力依存のテスト: テスト対象のメソッドが依存するオブジェクトから特定の値を返してほしい場合はスタブを使います
  • 出力検証のテスト: テスト対象のメソッドが依存するオブジェクトを正しく呼び出したかを確認したい場合はモック検証を使います
  • 完全なテスト: 多くの場合、スタブとモック検証を組み合わせて使用します

良いプラクティス

  1. テストを書く前に、「何をテストしたいか」を明確にする
  2. 入力依存の検証には when(...).thenReturn(...) でスタブを使用
  3. 出力の検証には verify(...) でモック検証を使用
  4. 必要以上に詳細な検証は避け、実装の詳細ではなく動作をテストする

複雑なテストケースでは、入力のスタブ設定と出力のモック検証を組み合わせることで、テスト対象のロジックを適切に検証できます。また、メソッドの呼び出し回数の検証には .called(n) を使用できます。

Discussion