🧪

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

2024/11/29に公開

dioでテストコードを書く

dioのテストコードを書くときは、http_mock_adapterを使うことがあるようだが、プロジェクトによっては、mockitoを使うのが指定されていることがあるので、こちらを今回は使用して簡単なテストコードを書いてみる。

こちらのサンプルを使用

テストコードに使うビジネスロジック

repository classにテストコードを作成。REST APIに対して、POSTとGETをおこなう処理が実装されております。全体のコードを知りたい人はGithubのサンプルを見てみてください。

import 'package:api_mock/domain/model/user_state.dart';
import 'package:api_mock/infrastructure/dio.dart';
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'user_repository.g.dart';

abstract interface class UserRepository {
  Future<List<UserState>> getUsers();
  Future<UserState> createUser(UserState user);
}

(keepAlive: true)
UserRepository userRepository(Ref ref) {
  return UserRepositoryImpl(ref);
}

class UserRepositoryImpl implements UserRepository {
  UserRepositoryImpl(this.ref);
  final Ref ref;

  
  Future<List<UserState>> getUsers() async {
    try {
      final response = await ref.read(dioProvider).get(
            '/',
            options: Options(method: 'GET'),
          );

      if (response.statusCode == 200) {
        if (response.data is List) {
          return (response.data as List)
              .map((json) => UserState.fromJson(json as Map<String, dynamic>))
              .toList();
        }

        throw Exception('Unexpected response format');
      } else {
        throw DioException(
          requestOptions: RequestOptions(path: '/'),
          response: response,
          error: 'Failed to fetch users: ${response.statusCode}',
        );
      }
    } catch (e) {
      throw Exception('Failed to fetch users: $e');
    }
  }

  Future<UserState> createUser(UserState user) async {
    try {
      final response = await ref.read(dioProvider).post(
            '/',
            data: user.toJson(),
            options: Options(
              headers: {'Content-Type': 'application/json'},
            ),
          );

      if (response.statusCode == 201) {
        return UserState.fromJson(response.data);
      }

      if (response.statusCode == 422) {
        // バリデーションエラーの処理
        final errors = response.data['errors'];
        throw Exception('Validation failed: $errors');
      }

      throw Exception('Failed to create user');
    } catch (e) {
      throw Exception('Failed to create user: $e');
    }
  }
}

テストコード

こちらがテストコードです。自動生成が必要なので途中で実行する。サンプルはMakefileを作成しているので、lutter pub run build_runner watch --delete-conflicting-outputsを短いコマンドで実行することができる。

// test/repository/user_repository_test.dart
import 'package:api_mock/domain/repository/user_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:dio/dio.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:riverpod/riverpod.dart';
import 'package:api_mock/domain/model/user_state.dart';
import 'package:api_mock/infrastructure/dio.dart';

@GenerateMocks([Dio])
import 'user_repository_test.mocks.dart';

void main() {
  late MockDio mockDio;
  late ProviderContainer container;

  setUp(() {
    mockDio = MockDio();
    container = ProviderContainer(
      overrides: [
        dioProvider.overrideWithValue(mockDio),
      ],
    );
  });

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

  group('UserRepository', () {
    test('getUsers returns list of users on successful response', () async {
      final mockResponse = [
        {'id': 1, 'name': 'Test User 1', 'email': 'test1@example.com'},
        {'id': 2, 'name': 'Test User 2', 'email': 'test2@example.com'},
      ];

      when(mockDio.get(
        any,
        options: anyNamed('options'),
      )).thenAnswer((_) async => Response(
            data: mockResponse,
            statusCode: 200,
            requestOptions: RequestOptions(path: '/'),
          ));

      final repository = container.read(userRepositoryProvider);
      final result = await repository.getUsers();

      expect(result, isA<List<UserState>>());
      expect(result.length, 2);
      expect(result[0].name, 'Test User 1');
      expect(result[1].name, 'Test User 2');

      verify(mockDio.get(
        any,
        options: anyNamed('options'),
      )).called(1);
    });

    test('getUsers throws exception on error response', () async {
      when(mockDio.get(
        any,
        options: anyNamed('options'),
      )).thenAnswer((_) async => Response(
            statusCode: 404,
            requestOptions: RequestOptions(path: '/'),
          ));

      final repository = container.read(userRepositoryProvider);

      expect(
        () => repository.getUsers(),
        throwsA(isA<Exception>()),
      );
    });
  });

  // POSTメソッドのテスト
  test('createUser creates a new user on successful response', () async {
    final newUser = UserState(
      name: 'New User',
      email: 'newuser@example.com',
    );

    final mockResponse = {
      'id': 1,
      'name': 'New User',
      'email': 'newuser@example.com',
    };

    when(mockDio.post(
      any,
      data: anyNamed('data'),
      options: anyNamed('options'),
    )).thenAnswer((_) async => Response(
          data: mockResponse,
          statusCode: 201,
          requestOptions: RequestOptions(path: '/'),
        ));

    final repository = container.read(userRepositoryProvider);
    final result = await repository.createUser(newUser);

    expect(result, isA<UserState>());
    expect(result.id, 1);
    expect(result.name, 'New User');
    expect(result.email, 'newuser@example.com');

    verify(mockDio.post(
      any,
      data: anyNamed('data'),
      options: anyNamed('options'),
    )).called(1);
  });

  test('createUser throws exception on error response', () async {
    final newUser = UserState(
      name: 'New User',
      email: 'newuser@example.com',
    );

    when(mockDio.post(
      any,
      data: anyNamed('data'),
      options: anyNamed('options'),
    )).thenAnswer((_) async => Response(
          statusCode: 400,
          requestOptions: RequestOptions(path: '/'),
        ));

    final repository = container.read(userRepositoryProvider);

    expect(
      () => repository.createUser(newUser),
      throwsA(isA<Exception>()),
    );
  });

  test('createUser handles validation errors', () async {
    final newUser = UserState(
      name: '', // 無効な名前
      email: 'invalid-email', // 無効なメール
    );

    final mockResponse = {
      'errors': {
        'name': ['Name is required'],
        'email': ['Invalid email format'],
      }
    };

    when(mockDio.post(
      any,
      data: anyNamed('data'),
      options: anyNamed('options'),
    )).thenAnswer((_) async => Response(
          data: mockResponse,
          statusCode: 422,
          requestOptions: RequestOptions(path: '/'),
        ));

    final repository = container.read(userRepositoryProvider);

    expect(
      () => repository.createUser(newUser),
      throwsA(isA<Exception>()),
    );

    verify(mockDio.post(
      any,
      data: anyNamed('data'),
      options: anyNamed('options'),
    )).called(1);
  });
}
make watch

Runボタンを押すとテストが実行できるので試してみてください。今回はstubとmockの実装をしてみました。

Stub

  • 特定の入力に対して、事前に定義された戻り値を返す
  • テストの「状態」を検証する(返されたデータが期待通りか)
  • 例:when(mockDio.get()).thenReturn(値)で、GETリクエストの戻り値を定義

Mock

  • メソッドが期待通りに呼び出されたかを検証する
  • テストの「振る舞い」を検証する(メソッドが正しく呼ばれたか)
  • 例:verify(mockDio.post()).called(1)で、POSTメソッドが1回呼ばれたことを確認

簡単な例:

// Stub - 戻り値の検証
when(mockDio.get('/users')).thenReturn(dummyResponse);
expect(result, dummyResponse);  // データの比較

// Mock - 呼び出しの検証
verify(mockDio.post('/users', data: userData)).called(1);  // メソッド呼び出しの確認

最後に

テストコードを仕事で書くことがあまりないので、日々の学びながら書いていこうと思います。今までテストコード書かない現場か既に書き終わっている現場しか知らない💦

Discussion