Riverpod测试支持:Flutter在鸿蒙平台编写可测试的状态管理代码
鸿蒙领航者
·


作者:付文龙(红目香薰)
仓库地址:https://gitcode.com/feng8403000/FlutterfromBeginnertoAdvancedForHarmonyOS.git
联系邮箱:372699828@qq.com
引言
测试是保证代码质量的重要手段。在Flutter应用开发中,状态管理代码的测试尤为重要,因为状态管理往往涉及复杂的业务逻辑和数据流。Riverpod作为一种声明式的状态管理方案,提供了出色的测试支持,使得编写可测试的状态管理代码变得更加简单和高效。本文将深入探讨如何在鸿蒙平台开发Flutter应用时,利用Riverpod的测试特性编写高质量的测试代码。
Riverpod测试优势
对比传统Provider测试
| 特性 | Provider | Riverpod |
|---|---|---|
| 测试复杂度 | 高 | 低 |
| 需要Widget树 | 是 | 否 |
| Mock支持 | 复杂 | 简单 |
| 异步测试 | 困难 | 简单 |
| 可读性 | 低 | 高 |
Riverpod测试核心工具
- ProviderContainer:创建测试环境,模拟ProviderScope。
- overrides:替换依赖,便于Mock。
- ref.watch/read:访问Provider状态。
- ref.invalidate:使Provider失效,触发重新计算。
测试环境设置
依赖配置
dev_dependencies:
flutter_test:
sdk: flutter
flutter_riverpod: ^2.4.9
mockito: ^5.4.4
build_runner: ^2.4.8
基本测试结构
import "package:flutter_test/flutter_test.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
void main() {
test("测试Provider", () {
final container = ProviderContainer();
// 测试逻辑
final value = container.read(myProvider);
expect(value, equals(expectedValue));
container.dispose();
});
}
StateNotifier测试
基本测试
class CartNotifier extends StateNotifier<List<String>> {
CartNotifier() : super([]);
void addItem(String item) {
state = [...state, item];
}
void removeItem(String item) {
state = state.where((i) => i != item).toList();
}
}
final cartProvider = StateNotifierProvider<CartNotifier, List<String>>((ref) {
return CartNotifier();
});
void main() {
group("CartNotifier", () {
late CartNotifier notifier;
setUp(() {
notifier = CartNotifier();
});
test("初始状态为空列表", () {
expect(notifier.state, equals([]));
});
test("添加商品后状态更新", () {
notifier.addItem("商品1");
expect(notifier.state, equals(["商品1"]));
});
test("移除商品后状态更新", () {
notifier.addItem("商品1");
notifier.removeItem("商品1");
expect(notifier.state, equals([]));
});
test("添加多个商品", () {
notifier.addItem("商品1");
notifier.addItem("商品2");
expect(notifier.state, equals(["商品1", "商品2"]));
});
});
}
使用ProviderContainer测试
void main() {
test("使用ProviderContainer测试", () {
final container = ProviderContainer();
// 读取状态
final cart = container.read(cartProvider);
expect(cart, equals([]));
// 修改状态
final notifier = container.read(cartProvider.notifier);
notifier.addItem("商品1");
// 验证状态更新
expect(container.read(cartProvider), equals(["商品1"]));
container.dispose();
});
}
Provider测试
测试计算属性
final countProvider = StateProvider<int>((ref) => 0);
final doubledCountProvider = Provider<int>((ref) {
final count = ref.watch(countProvider);
return count * 2;
});
void main() {
test("doubledCountProvider计算正确", () {
final container = ProviderContainer();
// 初始值
expect(container.read(doubledCountProvider), equals(0));
// 更新count
container.read(countProvider.notifier).state = 5;
expect(container.read(doubledCountProvider), equals(10));
// 再次更新
container.read(countProvider.notifier).state = 10;
expect(container.read(doubledCountProvider), equals(20));
container.dispose();
});
}
测试带依赖的Provider
final apiProvider = Provider<ApiService>((ref) => ApiService());
final userProvider = FutureProvider<User>((ref) async {
final api = ref.read(apiProvider);
return await api.getUser();
});
void main() {
test("userProvider返回用户数据", () async {
final container = ProviderContainer();
final user = await container.read(userProvider.future);
expect(user.name, equals("测试用户"));
container.dispose();
});
}
Mock依赖测试
使用Mock替换依赖
import "package:mockito/mockito.dart";
class MockApi extends Mock implements ApiService {
Future<User> getUser(String id) async {
return User(id: id, name: "Mock用户");
}
}
void main() {
test("使用Mock API", () async {
final mockApi = MockApi();
when(mockApi.getUser("1")).thenAnswer((_) async => User(id: "1", name: "Mock用户"));
final container = ProviderContainer(
overrides: [
apiProvider.overrideWithValue(mockApi),
],
);
final user = await container.read(userProvider("1").future);
expect(user.name, equals("Mock用户"));
verify(mockApi.getUser("1")).called(1);
container.dispose();
});
}
测试异步Provider
void main() {
test("测试异步Provider", () async {
final mockApi = MockApi();
when(mockApi.getUser("1")).thenAnswer((_) async {
await Future.delayed(Duration(milliseconds: 100));
return User(id: "1", name: "异步用户");
});
final container = ProviderContainer(
overrides: [
apiProvider.overrideWithValue(mockApi),
],
);
// 初始状态
expect(container.read(userProvider("1")).isLoading, isTrue);
// 等待完成
final user = await container.read(userProvider("1").future);
expect(user.name, equals("异步用户"));
container.dispose();
});
}
核心代码示例
示例1:完整的购物车测试
// 数据模型
class CartItem {
final String id;
final String name;
final double price;
final int quantity;
CartItem({
required this.id,
required this.name,
required this.price,
this.quantity = 1,
});
CartItem copyWith({int? quantity}) {
return CartItem(
id: id,
name: name,
price: price,
quantity: quantity ?? this.quantity,
);
}
bool operator ==(Object other) =>
identical(this, other) ||
other is CartItem &&
runtimeType == other.runtimeType &&
id == other.id &&
name == other.name &&
price == other.price &&
quantity == other.quantity;
int get hashCode => id.hashCode ^ name.hashCode ^ price.hashCode ^ quantity.hashCode;
}
// StateNotifier
class CartNotifier extends StateNotifier<List<CartItem>> {
CartNotifier() : super([]);
void addItem(CartItem item) {
final existingIndex = state.indexWhere((i) => i.id == item.id);
if (existingIndex >= 0) {
final newState = List<CartItem>.from(state);
newState[existingIndex] = newState[existingIndex].copyWith(
quantity: newState[existingIndex].quantity + 1,
);
state = newState;
} else {
state = [...state, item];
}
}
void removeItem(String id) {
state = state.where((item) => item.id != id).toList();
}
void updateQuantity(String id, int quantity) {
if (quantity <= 0) {
removeItem(id);
return;
}
state = state.map((item) {
if (item.id == id) {
return item.copyWith(quantity: quantity);
}
return item;
}).toList();
}
void clear() {
state = [];
}
}
// Provider
final cartProvider = StateNotifierProvider<CartNotifier, List<CartItem>>((ref) {
return CartNotifier();
});
final cartTotalProvider = Provider<double>((ref) {
final cart = ref.watch(cartProvider);
return cart.fold(0, (sum, item) => sum + item.price * item.quantity);
});
// 测试
void main() {
group("CartNotifier", () {
late CartNotifier notifier;
setUp(() {
notifier = CartNotifier();
});
test("初始状态为空", () {
expect(notifier.state, equals([]));
});
test("添加新商品", () {
final item = CartItem(id: "1", name: "商品1", price: 100);
notifier.addItem(item);
expect(notifier.state, equals([item]));
});
test("添加重复商品增加数量", () {
final item = CartItem(id: "1", name: "商品1", price: 100);
notifier.addItem(item);
notifier.addItem(item);
expect(notifier.state.length, equals(1));
expect(notifier.state[0].quantity, equals(2));
});
test("移除商品", () {
final item = CartItem(id: "1", name: "商品1", price: 100);
notifier.addItem(item);
notifier.removeItem("1");
expect(notifier.state, equals([]));
});
test("更新数量", () {
final item = CartItem(id: "1", name: "商品1", price: 100);
notifier.addItem(item);
notifier.updateQuantity("1", 5);
expect(notifier.state[0].quantity, equals(5));
});
test("数量为0时移除商品", () {
final item = CartItem(id: "1", name: "商品1", price: 100);
notifier.addItem(item);
notifier.updateQuantity("1", 0);
expect(notifier.state, equals([]));
});
test("清空购物车", () {
notifier.addItem(CartItem(id: "1", name: "商品1", price: 100));
notifier.addItem(CartItem(id: "2", name: "商品2", price: 200));
notifier.clear();
expect(notifier.state, equals([]));
});
});
group("cartTotalProvider", () {
test("计算总价", () {
final container = ProviderContainer();
final notifier = container.read(cartProvider.notifier);
// 初始总价
expect(container.read(cartTotalProvider), equals(0));
// 添加商品
notifier.addItem(CartItem(id: "1", name: "商品1", price: 100));
expect(container.read(cartTotalProvider), equals(100));
// 添加第二个商品
notifier.addItem(CartItem(id: "2", name: "商品2", price: 200));
expect(container.read(cartTotalProvider), equals(300));
// 增加数量
notifier.updateQuantity("1", 2);
expect(container.read(cartTotalProvider), equals(400));
// 移除商品
notifier.removeItem("1");
expect(container.read(cartTotalProvider), equals(200));
container.dispose();
});
});
}
示例2:用户认证测试
// 数据模型
class User {
final String id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
bool operator ==(Object other) =>
identical(this, other) ||
other is User &&
runtimeType == other.runtimeType &&
id == other.id &&
name == other.name &&
email == other.email;
int get hashCode => id.hashCode ^ name.hashCode ^ email.hashCode;
}
// API服务接口
abstract class ApiService {
Future<String> login(String email, String password);
Future<User> getCurrentUser();
}
// Storage服务接口
abstract class StorageService {
Future<String?> getToken();
Future<void> saveToken(String token);
Future<void> removeToken();
}
// Provider
final apiProvider = Provider<ApiService>((ref) => throw UnimplementedError());
final storageProvider = Provider<StorageService>((ref) => throw UnimplementedError());
// Auth状态
enum AuthStatus { loading, authenticated, unauthenticated }
class AuthState {
final AuthStatus status;
final User? user;
final String? error;
AuthState({required this.status, this.user, this.error});
AuthState copyWith({AuthStatus? status, User? user, String? error}) {
return AuthState(
status: status ?? this.status,
user: user ?? this.user,
error: error ?? this.error,
);
}
bool operator ==(Object other) =>
identical(this, other) ||
other is AuthState &&
runtimeType == other.runtimeType &&
status == other.status &&
user == other.user &&
error == other.error;
int get hashCode => status.hashCode ^ user.hashCode ^ error.hashCode;
}
// AuthNotifier
class AuthNotifier extends StateNotifier<AuthState> {
AuthNotifier(this.ref) : super(AuthState(status: AuthStatus.loading));
final Ref ref;
Future<void> login(String email, String password) async {
state = state.copyWith(status: AuthStatus.loading, error: null);
try {
final api = ref.read(apiProvider);
final token = await api.login(email, password);
final storage = ref.read(storageProvider);
await storage.saveToken(token);
final user = await api.getCurrentUser();
state = state.copyWith(status: AuthStatus.authenticated, user: user);
} catch (e) {
state = state.copyWith(
status: AuthStatus.unauthenticated,
error: e.toString(),
);
}
}
Future<void> logout() async {
final storage = ref.read(storageProvider);
await storage.removeToken();
state = state.copyWith(status: AuthStatus.unauthenticated, user: null);
}
}
// Provider定义
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier(ref);
});
// Mock类
class MockApi extends Mock implements ApiService {}
class MockStorage extends Mock implements StorageService {}
// 测试
void main() {
group("AuthNotifier", () {
late MockApi mockApi;
late MockStorage mockStorage;
late ProviderContainer container;
setUp(() {
mockApi = MockApi();
mockStorage = MockStorage();
container = ProviderContainer(
overrides: [
apiProvider.overrideWithValue(mockApi),
storageProvider.overrideWithValue(mockStorage),
],
);
});
tearDown(() {
container.dispose();
});
test("登录成功", () async {
// 设置Mock返回值
when(mockApi.login("test@example.com", "123456")).thenAnswer((_) async => "token");
when(mockStorage.saveToken("token")).thenAnswer((_) async {});
when(mockApi.getCurrentUser()).thenAnswer((_) async => User(
id: "1",
name: "测试用户",
email: "test@example.com",
));
// 执行登录
final notifier = container.read(authProvider.notifier);
await notifier.login("test@example.com", "123456");
// 验证状态
final authState = container.read(authProvider);
expect(authState.status, equals(AuthStatus.authenticated));
expect(authState.user?.name, equals("测试用户"));
// 验证Mock调用
verify(mockApi.login("test@example.com", "123456")).called(1);
verify(mockStorage.saveToken("token")).called(1);
verify(mockApi.getCurrentUser()).called(1);
});
test("登录失败", () async {
// 设置Mock抛出异常
when(mockApi.login("test@example.com", "wrong")).thenThrow(Exception("登录失败"));
// 执行登录
final notifier = container.read(authProvider.notifier);
await notifier.login("test@example.com", "wrong");
// 验证状态
final authState = container.read(authProvider);
expect(authState.status, equals(AuthStatus.unauthenticated));
expect(authState.error, equals("Exception: 登录失败"));
// 验证Mock调用
verify(mockApi.login("test@example.com", "wrong")).called(1);
verifyNever(mockStorage.saveToken(any));
verifyNever(mockApi.getCurrentUser());
});
test("登出", () async {
// 设置初始状态
when(mockStorage.removeToken()).thenAnswer((_) async {});
// 设置登录状态
container.read(authProvider.notifier).state = AuthState(
status: AuthStatus.authenticated,
user: User(id: "1", name: "测试用户", email: "test@example.com"),
);
// 执行登出
final notifier = container.read(authProvider.notifier);
await notifier.logout();
// 验证状态
final authState = container.read(authProvider);
expect(authState.status, equals(AuthStatus.unauthenticated));
expect(authState.user, isNull);
// 验证Mock调用
verify(mockStorage.removeToken()).called(1);
});
});
}
示例3:异步数据加载测试
// 数据模型
class Post {
final String id;
final String title;
final String content;
Post({required this.id, required this.title, required this.content});
bool operator ==(Object other) =>
identical(this, other) ||
other is Post &&
runtimeType == other.runtimeType &&
id == other.id &&
title == other.title &&
content == other.content;
int get hashCode => id.hashCode ^ title.hashCode ^ content.hashCode;
}
// API服务
abstract class ApiService {
Future<List<Post>> getPosts();
}
// Provider
final apiProvider = Provider<ApiService>((ref) => throw UnimplementedError());
final postsProvider = FutureProvider<List<Post>>((ref) async {
final api = ref.read(apiProvider);
return await api.getPosts();
});
// Mock类
class MockApi extends Mock implements ApiService {}
// 测试
void main() {
group("postsProvider", () {
late MockApi mockApi;
setUp(() {
mockApi = MockApi();
});
test("加载帖子列表", () async {
// 设置Mock返回值
final expectedPosts = [
Post(id: "1", title: "帖子1", content: "内容1"),
Post(id: "2", title: "帖子2", content: "内容2"),
];
when(mockApi.getPosts()).thenAnswer((_) async => expectedPosts);
// 创建容器
final container = ProviderContainer(
overrides: [
apiProvider.overrideWithValue(mockApi),
],
);
// 初始状态
expect(container.read(postsProvider).isLoading, isTrue);
// 等待完成
final posts = await container.read(postsProvider.future);
expect(posts, equals(expectedPosts));
// 验证完成状态
expect(container.read(postsProvider).hasValue, isTrue);
container.dispose();
});
test("加载失败", () async {
// 设置Mock抛出异常
when(mockApi.getPosts()).thenThrow(Exception("网络错误"));
// 创建容器
final container = ProviderContainer(
overrides: [
apiProvider.overrideWithValue(mockApi),
],
);
// 等待错误
expect(
container.read(postsProvider.future),
throwsA(isA<Exception>()),
);
// 验证错误状态
await Future.delayed(Duration(milliseconds: 100));
expect(container.read(postsProvider).hasError, isTrue);
expect(container.read(postsProvider).error.toString(), contains("网络错误"));
container.dispose();
});
test("多次加载", () async {
// 设置Mock返回不同的值
when(mockApi.getPosts()).thenAnswer((_) async {
await Future.delayed(Duration(milliseconds: 50));
return [Post(id: "1", title: "帖子1", content: "内容1")];
});
// 创建容器
final container = ProviderContainer(
overrides: [
apiProvider.overrideWithValue(mockApi),
],
);
// 第一次加载
final posts1 = await container.read(postsProvider.future);
expect(posts1.length, equals(1));
// 失效并重新加载
container.invalidate(postsProvider);
final posts2 = await container.read(postsProvider.future);
expect(posts2.length, equals(1));
// 验证调用次数
verify(mockApi.getPosts()).called(2);
container.dispose();
});
});
}
示例4:Widget测试
// CounterProvider
final countProvider = StateProvider<int>((ref) => 0);
// CounterWidget
class CounterWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(countProvider);
return Column(
children: [
Text("计数: $count"),
ElevatedButton(
onPressed: () {
ref.read(countProvider.notifier).state++;
},
child: Text("增加"),
),
],
);
}
}
// 测试
void main() {
testWidgets("CounterWidget测试", (tester) async {
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
home: Scaffold(
body: CounterWidget(),
),
),
),
);
// 初始状态
expect(find.text("计数: 0"), findsOneWidget);
// 点击按钮
await tester.tap(find.text("增加"));
await tester.pump();
// 状态更新
expect(find.text("计数: 1"), findsOneWidget);
// 再次点击
await tester.tap(find.text("增加"));
await tester.pump();
// 状态再次更新
expect(find.text("计数: 2"), findsOneWidget);
});
}
测试模式
单元测试
void main() {
test("单元测试", () {
final notifier = CartNotifier();
notifier.addItem(CartItem(id: "1", name: "商品", price: 100));
expect(notifier.state.length, equals(1));
});
}
集成测试
void main() {
test("集成测试", () async {
final container = ProviderContainer();
// 测试完整流程
final notifier = container.read(cartProvider.notifier);
notifier.addItem(CartItem(id: "1", name: "商品", price: 100));
final total = container.read(cartTotalProvider);
expect(total, equals(100));
container.dispose();
});
}
Widget测试
void main() {
testWidgets("Widget测试", (tester) async {
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(home: MyWidget()),
),
);
// 验证Widget行为
expect(find.text("Hello"), findsOneWidget);
});
}
最佳实践
1. 使用Mock替换依赖
test("测试", () {
final mockApi = MockApi();
final container = ProviderContainer(
overrides: [
apiProvider.overrideWithValue(mockApi),
],
);
});
2. 测试异步操作
test("测试异步", () async {
final user = await container.read(userProvider.future);
expect(user.name, equals("测试用户"));
});
3. 验证Mock调用
test("验证调用", () async {
await notifier.login("email", "password");
verify(mockApi.login("email", "password")).called(1);
});
4. 使用group组织测试
void main() {
group("CartNotifier", () {
test("测试1", () {});
test("测试2", () {});
});
group("cartTotalProvider", () {
test("测试1", () {});
});
}
5. 清理资源
void main() {
test("测试", () {
final container = ProviderContainer();
// 测试逻辑
container.dispose(); // 清理资源
});
}
总结
Riverpod的测试支持为Flutter应用开发提供了强大的测试能力:
- ProviderContainer:创建独立的测试环境,无需Widget树。
- overrides:灵活替换依赖,便于Mock。
- 同步测试:StateNotifier可以直接测试,无需异步等待。
- 异步测试:FutureProvider和StreamProvider支持异步测试。
- Widget测试:可以与Flutter的Widget测试框架集成。
通过合理使用这些测试工具,可以编写高质量的测试代码,确保状态管理逻辑的正确性。在鸿蒙平台开发Flutter应用时,这些测试特性将帮助你构建更加可靠和稳定的应用。
更多推荐


所有评论(0)