🧪
dio & mockitoでテストコードを書く
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