
本文详细介绍了如何在flutter应用中,利用mockito和dio库对post请求(特别是登录认证场景)进行单元测试。内容涵盖了dio请求的实现、mockito的模拟技巧,以及如何构造模拟响应来验证业务逻辑,确保代码质量和可维护性。
在Flutter应用开发中,网络请求是常见的业务场景。为了保证代码的健壮性和可维护性,对涉及网络请求的服务进行单元测试至关重要。本文将以一个使用Dio库进行登录认证的POST请求为例,详细讲解如何结合Mockito进行有效的单元测试。
1. 理解Dio POST请求的认证逻辑
首先,我们来看一个典型的登录认证服务方法,它使用Dio库发送POST请求:
import 'package:dio/dio.dart';
import 'package:fluttertoast/fluttertoast.dart'; // 注意:在单元测试中应避免直接调用UI相关方法
// 假设Apiservices包含基础URL
class Apiservices {
final String baseurl = "https://api.example.com/auth"; // 示例URL
// 其他API方法...
}
class AuthService {
// 推荐通过构造函数注入Dio实例,方便测试
final Apiservices _apiServices;
AuthService(this._apiServices);
Future authentication(String mobileNumber, String password, Dio dio) async {
dynamic data = {
"username": mobileNumber,
"password": password,
"grant_type": "password",
"client_id": "client_id"
};
try {
final response = await dio.post(_apiServices.baseurl, // 使用传入的dio实例
data: data,
options: Options(headers: {
"Content-Type": "application/x-www-form-urlencoded"
}));
if (response.statusCode == 200) {
// 假设这里会处理access_token和refresh_token,并可能存储起来
// 为了单元测试的纯粹性,这里直接返回access_token
String? accessToken = response.data["access_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; // 认证失败或异常情况返回null
}
} 关键点解析:
- authentication 方法接收 mobileNumber, password 和一个 Dio 实例。通过参数传递 Dio 实例是实现可测试性的关键,这被称为依赖注入。
- 它构建了一个表单编码的请求体 (data)。
- 使用 dio.post 发送请求。
- 根据 response.statusCode 判断请求是否成功。
- 成功时返回 access_token,失败或异常时捕获 DioError 并返回其字符串表示。
- 注意:在实际的单元测试中,应避免直接调用 Fluttertoast.showToast 这类与UI或平台相关的副作用方法,因为它们难以测试且会引入不必要的复杂性。如果需要测试消息提示,应将消息提示逻辑抽象成一个可注入的服务。
2. Mockito与Dio单元测试基础
单元测试的目标是隔离被测试代码,确保其逻辑的正确性,而不受外部依赖(如网络、数据库)的影响。Mockito是一个流行的Dart和Flutter测试框架,用于创建模拟对象(Mock Objects)。
为了测试 AuthService 中的 authentication 方法,我们需要模拟 Dio 实例的行为,使其在调用 post 方法时返回预设的响应,而不是发起真实的网络请求。
创建Mock Dio类:
import 'package:dio/dio.dart';
import 'package:mockito/mockito.dart';
// 使用build_runner生成Mock类,或者手动实现
// 如果使用build_runner,需要创建一个mock_dio.dart文件并包含:
// @GenerateMocks([Dio])
// void main() {}
// 然后运行 `flutter pub run build_runner build`
// 这里我们直接手动实现一个简单的Mock类,或者假设它已经生成
class MockDio extends Mock implements Dio {}3. 模拟Dio POST请求的步骤
一个标准的单元测试通常遵循“Arrange-Act-Assert”(准备-执行-断言)模式。
3.1 Arrange (准备阶段)
在这一阶段,我们设置测试环境,包括初始化模拟对象和定义它们的行为。
- 初始化Mock对象: 创建 MockDio 实例。
- 定义模拟响应数据: 准备一个模拟的成功或失败的JSON响应数据。
-
使用 when().thenAnswer() 模拟Dio行为:
- when(mockDio.post(url, data: anyNamed('data'), options: anyNamed('options'))):指定当 mockDio 的 post 方法被调用时,且匹配特定的URL和可选的 data/options 参数时。anyNamed 是Mockito的一个匹配器,表示可以匹配任何值。
- .thenAnswer((_) async => Response(...)):定义当上述条件满足时,post 方法应返回一个 Future
。我们需要构造一个 Dio 的 Response 对象,包含模拟的状态码和数据。
3.2 Act (执行阶段)
在此阶段,我们调用被测试的方法,传入模拟对象。
- 调用 AuthService 实例的 authentication 方法,并传入 MockDio 实例。
3.3 Assert (断言阶段)
最后,我们验证被测试方法的输出或状态是否符合预期。
- 使用 expect() 函数检查方法的返回值是否正确(例如,是否返回了预期的access token,或者在错误情况下返回了正确的错误信息)。
- 也可以使用 verify() 来检查 mockDio.post 是否被调用了预期的次数和参数。
4. 示例代码:登录认证的单元测试
以下是针对 AuthService 的 authentication 方法的完整单元测试示例,涵盖了成功和失败两种情况:
import 'package:flutter_test/flutter_test.dart';
import 'package:dio/dio.dart';
import 'package:mockito/mockito.dart';
import 'dart:convert'; // 用于jsonEncode/jsonDecode
// 导入你的服务类和API服务类
// import 'path/to/your/auth_service.dart';
// import 'path/to/your/api_services.dart';
// --- Mock类定义 (假设已在其他文件或顶部定义) ---
class MockDio extends Mock implements Dio {}
class Apiservices {
final String baseurl = "https://api.example.com/auth";
}
class AuthService {
final Apiservices _apiServices;
AuthService(this._apiServices);
Future authentication(String mobileNumber, String password, Dio dio) 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) {
String? accessToken = response.data["access_token"];
return accessToken;
}
} on DioError catch (e) {
return e.toString();
}
return null;
}
}
// --- Mock类定义结束 ---
void main() {
group('AuthService', () {
late MockDio mockDio;
late Apiservices apiServices;
late AuthService authService;
// 在每个测试运行前执行,用于初始化
setUp(() {
mockDio = MockDio();
apiServices = Apiservices();
authService = AuthService(apiServices); // 注入apiServices
});
// 定义一个成功的登录响应数据
final Map loginSuccessResponse = {
"access_token": "mock_access_token_123",
"refresh_token": "mock_refresh_token_456",
"expires_in": 3600,
"token_type": "Bearer"
};
// 定义一个失败的登录响应数据 (例如401 Unauthorized)
final Map loginFailureResponse = {
"error": "unauthorized",
"message": "Invalid credentials"
};
test('当登录成功时,应该返回access token', () async {
// Arrange (准备阶段)
// 模拟Dio.post方法返回一个成功的Response
when(mockDio.post(
apiServices.baseurl,
data: anyNamed('data'), // 匹配任何数据负载
options: anyNamed('options'), // 匹配任何Options
)).thenAnswer((_) async => Response(
requestOptions: RequestOptions(path: apiServices.baseurl), // 必须提供RequestOptions
data: loginSuccessResponse,
statusCode: 200,
));
// Act (执行阶段)
final result = await authService.authentication("testuser", "testpass", mockDio);
// Assert (断言阶段)
expect(result, isA());
expect(result, "mock_access_token_123"); // 验证返回了预期的access token
// 验证Dio.post方法是否被调用了一次
verify(mockDio.post(
apiServices.baseurl,
data: anyNamed('data'),
options: anyNamed('options'),
)).called(1);
});
test('当登录失败(例如401 Unauthorized)时,应该返回DioError的字符串表示', () async {
// Arrange (准备阶段)
// 构造一个DioError来模拟网络请求失败
final DioError dioError = DioError(
requestOptions: RequestOptions(path: apiServices.baseurl),
response: Response(
requestOptions: RequestOptions(path: apiServices.baseurl),
statusCode: 401,
data: loginFailureResponse,
),
type: DioErrorType.response,
error: "HTTP status error [401]",
);
// 模拟Dio.post方法抛出DioError
when(mockDio.post(
apiServices.baseurl,
data: anyNamed('data'),
options: anyNamed('options'),
)).thenThrow(dioError);
// Act (执行阶段)
final result = await authService.authentication("wronguser", "wrongpass", mockDio);
// Assert (断言阶段)
expect(result, isA());
expect(result, dioError.toString()); // 验证返回了DioError的字符串表示
// 验证Dio.post方法是否被调用了一次
verify(mockDio.post(
apiServices.baseurl,
data: anyNamed('data'),
options: anyNamed('options'),
)).called(1);
});
test('当网络连接异常时,应该返回DioError的字符串表示', () async {
// Arrange
final DioError networkError = DioError(
requestOptions: RequestOptions(path: apiServices.baseurl),
type: DioErrorType.other, // 模拟网络连接问题
error: "SocketException: Failed host lookup",
);
when(mockDio.post(
apiServices.baseurl,
data: anyNamed('data'),
options: anyNamed('options'),
)).thenThrow(networkError);
// Act
final result = await authService.authentication("anyuser", "anypass", mockDio);
// Assert
expect(result, isA());
expect(result, networkError.toString());
verify(mockDio.post(
apiServices.baseurl,
data: anyNamed('data'),
options: anyNamed('options'),
)).called(1);
});
});
} 5. 注意事项与最佳实践
- 依赖注入: 始终通过构造函数或方法参数注入 Dio 实例,而不是在服务类内部直接创建,这大大提高了代码的可测试性。
- 避免副作用: 在被测试的服务方法中,尽量避免直接调用如 Fluttertoast.showToast 或修改全局状态的副作用操作。如果必须有这些行为,考虑将其抽象为可注入的接口,并在测试中模拟该接口。
-
精确匹配与通用匹配:
- when(mockDio.post(url, data: anyNamed('data'), options: anyNamed('options'))) 是一种通用的匹配方式,可以匹配任何 data 和 options。
- 如果你需要测试 post 方法是否携带了特定的 data 或 options,可以使用 argThat(equals(expectedData)) 或自定义匹配器。
- 完整的 Response 对象: 构造 Dio.Response 时,requestOptions 是必需的,即使在简单测试中也可以提供一个默认的 RequestOptions(path: 'test')。
- 测试错误处理: 除了成功的场景,务必测试各种错误情况,如HTTP状态码错误(4xx, 5xx)、网络连接错误(DioErrorType.other)、超时等。通过 `thenThrow(DioError(...










