在这里插入图片描述
在这里插入图片描述

作者:付文龙(红目香薰)
仓库地址https://gitcode.com/feng8403000/FlutterfromBeginnertoAdvancedForHarmonyOS.git
联系邮箱:372699828@qq.com

引言

测试是保证代码质量的重要手段。在Flutter应用开发中,状态管理代码的测试尤为重要,因为状态管理往往涉及复杂的业务逻辑和数据流。Riverpod作为一种声明式的状态管理方案,提供了出色的测试支持,使得编写可测试的状态管理代码变得更加简单和高效。本文将深入探讨如何在鸿蒙平台开发Flutter应用时,利用Riverpod的测试特性编写高质量的测试代码。

Riverpod测试优势

对比传统Provider测试

特性 Provider Riverpod
测试复杂度
需要Widget树
Mock支持 复杂 简单
异步测试 困难 简单
可读性

Riverpod测试核心工具

  1. ProviderContainer:创建测试环境,模拟ProviderScope。
  2. overrides:替换依赖,便于Mock。
  3. ref.watch/read:访问Provider状态。
  4. 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应用开发提供了强大的测试能力:

  1. ProviderContainer:创建独立的测试环境,无需Widget树。
  2. overrides:灵活替换依赖,便于Mock。
  3. 同步测试:StateNotifier可以直接测试,无需异步等待。
  4. 异步测试:FutureProvider和StreamProvider支持异步测试。
  5. Widget测试:可以与Flutter的Widget测试框架集成。

通过合理使用这些测试工具,可以编写高质量的测试代码,确保状态管理逻辑的正确性。在鸿蒙平台开发Flutter应用时,这些测试特性将帮助你构建更加可靠和稳定的应用。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐