
本教程将详细指导如何在flutter应用中,使用mockito框架对基于dio库实现的post网络请求进行单元测试,特别是针对登录认证场景。文章将演示如何模拟dio的`post`方法,返回预期的响应数据,从而验证业务逻辑的正确性,确保代码质量和可维护性。
在Flutter开发中,网络请求是常见的业务场景,而Dio是一个功能强大且广泛使用的HTTP客户端。为了确保应用的网络交互逻辑健壮可靠,进行单元测试至关重要。本教程将以一个登录认证的POST请求为例,详细讲解如何结合Mockito库来模拟Dio的行为,从而在不依赖真实网络的情况下测试我们的业务逻辑。
1. 理解待测试的认证服务
首先,我们来看一个典型的登录认证服务。该服务使用Dio发起POST请求,尝试通过用户名和密码获取访问令牌。
import 'package:dio/dio.dart';
import 'package:fluttertoast/fluttertoast.dart'; // 实际测试中通常会mock或忽略UI组件
// 假设的API服务类,提供基础URL
class Apiservices {
final String baseurl = "https://api.example.com/auth/token"; // 替换为你的实际登录API
}
class AuthenticationService {
final Dio _dio;
String? accessToken;
String? refreshToken;
final Apiservices _apiServices = Apiservices();
AuthenticationService(this._dio); // 注入Dio实例
/// 发起登录认证请求
/// 成功则返回accessToken,失败则返回DioError的字符串表示
Future authentication(String mobileNumber, String password) async {
dynamic data = {
"username": mobileNumber,
"password": password,
"grant_type": "password",
"client_id": "client_id"
};
try {
final response = await _dio.post(
_apiServices.baseurl,
data: data,
options: Options(headers: {
"Content-Type": "application/x-www-form-urlencoded"
}),
);
if (response.statusCode == 200) {
accessToken = response.data["access_token"];
refreshToken = response.data["refresh_token"];
// Fluttertoast.showToast(msg: "Successfully Logged in"); // UI交互,单元测试中通常不直接测试
return accessToken;
}
} on DioError catch (e) {
// Fluttertoast.showToast(msg: "Something went wrong"); // UI交互
return e.toString(); // 返回错误信息字符串
}
return null; // 如果try块中没有返回,且没有发生DioError,则返回null
}
} 在这个AuthenticationService中,authentication方法负责构建请求体、发送POST请求,并根据响应状态码处理结果。在单元测试中,我们希望模拟_dio.post的行为,以验证authentication方法在不同响应下的逻辑。
2. 设置单元测试环境
为了进行单元测试,我们需要以下依赖:
- flutter_test: Flutter的测试框架。
- mockito: 用于创建模拟对象的库。
- dio: 被测试的服务所依赖的网络库。
- build_runner 和 mockito_annotations: 用于自动生成Mockito模拟类。
在pubspec.yaml中添加或更新依赖:
dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^5.4.4 # 请使用最新版本
build_runner: ^2.4.8 # 请使用最新版本
mockito_annotations: ^2.3.0 # 请使用最新版本然后运行 flutter pub get。
接下来,我们需要为Dio类生成一个模拟类。在你的测试文件(例如 test/authentication_service_test.dart)的顶部,添加以下内容:
// test/authentication_service_test.dart import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:dio/dio.dart'; import 'package:mockito/annotations.dart'; // 导入你的服务文件 import 'package:your_app_name/authentication_service.dart'; // 替换为你的实际路径 // 使用 @GenerateMocks 注解为 Dio 类生成 Mock 类 @GenerateMocks([Dio]) import 'authentication_service_test.mocks.dart'; // 这将在运行 build_runner 后生成
在添加 @GenerateMocks 注解后,运行以下命令来生成模拟文件: flutter pub run build_runner build 这会生成一个 authentication_service_test.mocks.dart 文件,其中包含 MockDio 类。
3. 使用Mockito模拟Dio的POST请求
现在我们可以编写单元测试了。核心思想是使用 when().thenAnswer() 或 when().thenThrow() 来控制 MockDio 对象的行为。
3.1 模拟成功响应
当登录请求成功时,我们期望authentication方法返回一个访问令牌。
// 模拟成功的登录响应数据 final MaploginSuccessResponse = { "access_token": "mock_access_token_123", "refresh_token": "mock_refresh_token_456", "expires_in": 3600, "token_type": "Bearer" }; void main() { late MockDio mockDio; late AuthenticationService authService; // 在每个测试运行前初始化模拟对象和待测试服务 setUp(() { mockDio = MockDio(); authService = AuthenticationService(mockDio); }); group('AuthenticationService', () { test('认证成功时应返回访问令牌并设置accessToken', () async { // Arrange (安排): 设置模拟Dio的行为 when(mockDio.post( Apiservices().baseurl, // 匹配请求的URL data: anyNamed('data'), // 匹配任何名为'data'的参数 options: anyNamed('options'), // 匹配任何名为'options'的参数 )).thenAnswer((_) async => Response( requestOptions: RequestOptions(path: Apiservices().baseurl), // 必须提供 RequestOptions data: loginSuccessResponse, statusCode: 200, )); // Act (执行): 调用待测试的方法 final result = await authService.authentication("test_mobile", "test_password"); // Assert (断言): 验证结果是否符合预期 expect(result, isA ()); // 期望返回一个字符串 expect(result, loginSuccessResponse["access_token"]); // 期望返回正确的访问令牌 expect(authService.accessToken, loginSuccessResponse["access_token"]); // 验证服务内部的accessToken是否被设置 // 验证 Dio 的 post 方法是否被正确调用了一次 verify(mockDio.post( Apiservices().baseurl, data: anyNamed('data'), options: anyNamed('options'), )).called(1); }); }); }
代码解释:
- setUp(): 在每个测试用例运行前,初始化 MockDio 和 AuthenticationService 实例,确保测试之间的隔离性。
- when(mockDio.post(...)).thenAnswer(...): 这是Mockito的核心。我们告诉 mockDio,当它的 post 方法被调用时,应该执行 thenAnswer 中定义的逻辑。
- Apiservices().baseurl: 确保我们匹配的是服务中实际使用的URL。
- anyNamed('data'), anyNamed('options'): 匹配 post 方法中 data 和 options 参数的任何值。如果需要更精确的匹配,可以使用 argThat(equals(...)) 或 captureAny。
- thenAnswer((_) async => Response(...)): 返回一个 Future
。我们手动构造一个 Response 对象,包含预期的状态码(200)和数据。RequestOptions 是 Response 构造函数所必需的。
- authService.authentication(...): 调用我们正在测试的实际方法。
- expect(): 断言结果。我们检查返回类型、返回值以及 authService 内部 accessToken 属性是否被正确更新。
- verify().called(1): 这是一个重要的验证步骤,确保 mockDio.post 方法确实被调用了一次。
3.2 模拟失败响应(DioError)
当登录请求失败时,例如服务器返回400错误或网络连接问题,我们期望authentication方法捕获DioError并返回其字符串表示。
// 模拟失败的登录响应数据 final MaploginFailureResponse = { "error": "invalid_grant", "error_description": "Bad credentials" }; // ... (main, setUp, group 保持不变) group('AuthenticationService', () { // ... (成功测试用例) test('认证失败时应返回DioError的字符串表示', () async { // Arrange: 设置模拟Dio在post请求时抛出 DioError final dioError = DioError( requestOptions: RequestOptions(path: Apiservices().baseurl), response: Response( requestOptions: RequestOptions(path: Apiservices().baseurl), data: loginFailureResponse, // 可以包含错误响应体 statusCode: 400, ), type: DioErrorType.response, // 指定错误类型 error: "Bad credentials", // 错误信息 ); when(mockDio.post( Apiservices().baseurl, data: anyNamed('data'), options: anyNamed('options'), )).thenThrow(dioError); // 模拟抛出 DioError // Act: 调用待测试的方法 final result = await authService.authentication("wrong_mobile", "wrong_password"); // Assert: 验证结果 expect(result, isA ()); // 期望返回一个字符串 expect(result, contains("DioError")); // 期望字符串中包含 "DioError" expect(result, contains("Bad credentials")); // 期望包含具体的错误信息 expect(authService.accessToken, isNull); // 验证accessToken未被设置 verify(mockDio.post( Apiservices().baseurl, data: anyNamed('data'), options: anyNamed('options'), )).called(1); }); });
代码解释:
- thenThrow(dioError): 这次我们让 mockDio.post 方法抛出一个 DioError 实例,而不是返回一个 Response。
- 我们构造了一个 DioError 对象,其中包含 RequestOptions、可选的 Response(用于表示HTTP错误,如400)、type 和 error 属性。
- 断言部分检查返回的字符串是否包含 DioError 关键字以及具体的错误描述。
4. 完整示例代码
以下是包含所有必要部分的完整单元测试文件示例:
// test/authentication_service_test.mocks.dart (此文件由 build_runner 自动生成,请勿手动修改)
// import 'package:mockito/mockito.dart';
// import 'package:dio/dio.dart';
// class MockDio extends Mock implements Dio {}
// test/authentication_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:dio/dio.dart';
import 'package:mockito/annotations.dart';
// 导入你的服务文件
import 'package:your_app_name/authentication_service.dart'; // 替换为你的实际路径
// 使用 @GenerateMocks 注解为 Dio 类生成 Mock 类
@GenerateMocks([Dio])
import 'authentication_service_test.mocks.dart'; // 运行 `flutter pub run build_runner build` 后生成
// 模拟成功的登录响应数据
final Map loginSuccessResponse = {
"access_token": "mock_access_token_123",
"refresh_token": "mock_refresh_token_456",
"expires_in": 3600,
"token_type": "Bearer"
};
// 模拟失败的登录响应数据
final Map loginFailureResponse = {
"error": "invalid_grant",
"error_description": "Bad credentials"
};
void main() {
late MockDio mockDio;
late AuthenticationService authService;
// 在每个测试运行前初始化模拟对象和待测试服务
setUp(() {
mockDio = MockDio();
authService = AuthenticationService(mockDio);
});
group('AuthenticationService', () {
test('认证成功时应返回访问令牌并设置accessToken', () async {
// Arrange (安排):










