异步Redux视图模型测试Skill asyncredux-testing-view-models

异步Redux视图模型测试技能专注于在Flutter应用中测试StoreConnector的视图模型,包括使用Vm.createFrom()创建视图模型、验证属性、测试回调动作分派和状态变化验证。适用于Dart/Flutter开发中的单元测试,提升代码质量和可维护性。关键词:AsyncRedux, 视图模型测试, Dart, Flutter, 单元测试, StoreConnector, VmFactory, 移动应用测试。

测试 0 次安装 0 次浏览 更新于 3/19/2026

名称: 异步redux测试视图模型 描述: 在隔离环境中测试StoreConnector视图模型。涵盖使用Vm.createFrom()创建视图模型、测试视图模型属性、测试分派动作的回调以及验证回调中的状态变化。

在AsyncRedux中测试视图模型

VmFactory创建的视图模型可以在不构建小部件的情况下进行隔离测试。使用Vm.createFrom()直接实例化视图模型,然后验证属性和执行回调。

为测试创建视图模型

使用Vm.createFrom()与存储和工厂实例:

import 'package:flutter_test/flutter_test.dart';
import 'package:async_redux/async_redux.dart';

test('视图模型具有正确属性', () {
  var store = Store<AppState>(
    initialState: AppState(name: 'Mary', counter: 5),
  );

  var vm = Vm.createFrom(store, CounterFactory());

  expect(vm.counter, 5);
  expect(vm.name, 'Mary');
});

重要: Vm.createFrom()每个工厂实例只能调用一次。为每个测试创建一个新工厂。

测试视图模型属性

验证工厂正确将状态转换为视图模型属性:

class CounterViewModel extends Vm {
  final int counter;
  final String description;
  final VoidCallback onIncrement;

  CounterViewModel({
    required this.counter,
    required this.description,
    required this.onIncrement,
  }) : super(equals: [counter, description]);
}

class CounterFactory extends VmFactory<AppState, CounterConnector, CounterViewModel> {
  @override
  CounterViewModel fromStore() => CounterViewModel(
    counter: state.counter,
    description: '计数为 ${state.counter}',
    onIncrement: () => dispatch(IncrementAction()),
  );
}

test('工厂正确转换状态', () {
  var store = Store<AppState>(
    initialState: AppState(counter: 10),
  );

  var vm = Vm.createFrom(store, CounterFactory());

  expect(vm.counter, 10);
  expect(vm.description, '计数为 10');
});

测试分派动作的回调

测试回调时,调用它们,然后使用等待方法验证动作被分派和状态改变:

test('onIncrement分派IncrementAction', () async {
  var store = Store<AppState>(
    initialState: AppState(counter: 0),
  );

  var vm = Vm.createFrom(store, CounterFactory());

  // 调用回调
  vm.onIncrement();

  // 等待动作完成
  await store.waitActionType(IncrementAction);

  // 验证状态改变
  expect(store.state.counter, 1);
});

回调测试的等待方法

几种等待方法帮助验证回调行为:

waitActionType

等待特定动作类型完成:

test('回调分派预期动作', () async {
  var store = Store<AppState>(initialState: AppState(name: ''));
  var vm = Vm.createFrom(store, UserFactory());

  vm.onSave('John');
  await store.waitActionType(SaveNameAction);

  expect(store.state.name, 'John');
});

waitAllActionTypes

等待多个动作类型完成:

test('回调触发多个动作', () async {
  var store = Store<AppState>(initialState: AppState.initialState());
  var vm = Vm.createFrom(store, CheckoutFactory());

  vm.onCheckout();
  await store.waitAllActionTypes([ValidateCartAction, ProcessPaymentAction]);

  expect(store.state.orderCompleted, isTrue);
});

waitAnyActionTypeFinishes

等待任何匹配动作完成,适用于测试可能分派的动作:

test('刷新触发数据获取', () async {
  var store = Store<AppState>(initialState: AppState.initialState());
  var vm = Vm.createFrom(store, DataFactory());

  vm.onRefresh();
  var action = await store.waitAnyActionTypeFinishes([FetchDataAction]);

  expect(action, isA<FetchDataAction>());
  expect(store.state.data, isNotEmpty);
});

waitCondition

等待状态满足特定条件:

test('加载完成当数据被获取', () async {
  var store = Store<AppState>(initialState: AppState(isLoading: false, data: null));
  var vm = Vm.createFrom(store, DataFactory());

  vm.onLoad();
  await store.waitCondition((state) => state.data != null);

  expect(store.state.isLoading, isFalse);
  expect(store.state.data, isNotNull);
});

waitAllActions

等待直到没有动作在进行中:

test('所有动作完成', () async {
  var store = Store<AppState>(initialState: AppState.initialState());
  var vm = Vm.createFrom(store, BatchFactory());

  vm.onProcessBatch();
  await store.waitAllActions([]);

  expect(store.state.batchProcessed, isTrue);
});

测试带动作状态的回调

验证回调分派动作成功或失败:

test('保存回调处理错误', () async {
  var store = Store<AppState>(
    initialState: AppState(data: ''),
  );

  var vm = Vm.createFrom(store, FormFactory());

  // 使用无效数据触发保存
  vm.onSave('');

  // dispatchAndWait返回ActionStatus,但测试回调时,
  // 使用waitActionType并检查store.errors
  await store.waitActionType(SaveAction);

  // 检查动作是否失败
  expect(store.errors, isNotEmpty);
});

测试异步回调

异步回调以相同方式工作 - 等待分派的动作:

class UserFactory extends VmFactory<AppState, UserConnector, UserViewModel> {
  @override
  UserViewModel fromStore() => UserViewModel(
    user: state.user,
    onRefresh: () => dispatch(FetchUserAction()),
  );
}

test('onRefresh加载用户数据', () async {
  var store = Store<AppState>(
    initialState: AppState(user: null),
  );

  var vm = Vm.createFrom(store, UserFactory());

  vm.onRefresh();
  await store.waitActionType(FetchUserAction);

  expect(store.state.user, isNotNull);
});

使用模拟动作进行测试

使用MockStore模拟回调触发的动作:

test('带模拟依赖的回调', () async {
  var store = MockStore<AppState>(
    initialState: AppState(data: null),
    mocks: {
      // 模拟API调用返回测试数据
      FetchDataAction: (action, state) => state.copy(data: '模拟数据'),
    },
  );

  var vm = Vm.createFrom(store, DataFactory());

  vm.onFetch();
  await store.waitActionType(FetchDataAction);

  expect(store.state.data, '模拟数据');
});

测试onInit和onDispose生命周期

使用ConnectorTester测试生命周期回调而不构建小部件:

class MyScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) => StoreConnector<AppState, MyViewModel>(
    vm: () => MyFactory(),
    onInit: (store) => store.dispatch(StartPollingAction()),
    onDispose: (store) => store.dispatch(StopPollingAction()),
    builder: (context, vm) => MyWidget(vm: vm),
  );
}

test('onInit分派StartPollingAction', () async {
  var store = Store<AppState>(initialState: AppState.initialState());
  var connectorTester = store.getConnectorTester(MyScreen());

  connectorTester.runOnInit();
  var action = await store.waitAnyActionTypeFinishes([StartPollingAction]);

  expect(action, isA<StartPollingAction>());
});

test('onDispose分派StopPollingAction', () async {
  var store = Store<AppState>(initialState: AppState.initialState());
  var connectorTester = store.getConnectorTester(MyScreen());

  connectorTester.runOnDispose();
  var action = await store.waitAnyActionTypeFinishes([StopPollingAction]);

  expect(action, isA<StopPollingAction>());
});

完整测试示例

import 'package:flutter_test/flutter_test.dart';
import 'package:async_redux/async_redux.dart';

// 视图模型
class TodoViewModel extends Vm {
  final List<String> todos;
  final bool isLoading;
  final void Function(String) onAddTodo;
  final void Function(int) onRemoveTodo;
  final VoidCallback onRefresh;

  TodoViewModel({
    required this.todos,
    required this.isLoading,
    required this.onAddTodo,
    required this.onRemoveTodo,
    required this.onRefresh,
  }) : super(equals: [todos, isLoading]);
}

// 工厂
class TodoFactory extends VmFactory<AppState, TodoConnector, TodoViewModel> {
  @override
  TodoViewModel fromStore() => TodoViewModel(
    todos: state.todos,
    isLoading: state.isLoading,
    onAddTodo: (text) => dispatch(AddTodoAction(text)),
    onRemoveTodo: (index) => dispatch(RemoveTodoAction(index)),
    onRefresh: () => dispatch(FetchTodosAction()),
  );
}

void main() {
  group('TodoFactory', () {
    late Store<AppState> store;

    setUp(() {
      store = Store<AppState>(
        initialState: AppState(todos: [], isLoading: false),
      );
    });

    test('使用正确的初始属性创建视图模型', () {
      var vm = Vm.createFrom(store, TodoFactory());

      expect(vm.todos, isEmpty);
      expect(vm.isLoading, isFalse);
    });

    test('onAddTodo分派AddTodoAction', () async {
      var vm = Vm.createFrom(store, TodoFactory());

      vm.onAddTodo('买牛奶');
      await store.waitActionType(AddTodoAction);

      expect(store.state.todos, contains('买牛奶'));
    });

    test('onRemoveTodo分派RemoveTodoAction', () async {
      store = Store<AppState>(
        initialState: AppState(todos: ['任务1', '任务2'], isLoading: false),
      );
      var vm = Vm.createFrom(store, TodoFactory());

      vm.onRemoveTodo(0);
      await store.waitActionType(RemoveTodoAction);

      expect(store.state.todos, ['任务2']);
    });

    test('onRefresh获取待办事项', () async {
      var vm = Vm.createFrom(store, TodoFactory());

      vm.onRefresh();
      await store.waitCondition((state) => !state.isLoading);

      expect(store.state.todos, isNotEmpty);
    });
  });
}

测试组织

遵循推荐的文件命名约定:

  • 小部件: todo_screen.dart
  • 连接器: todo_screen_connector.dart
  • 状态测试: todo_screen_STATE_test.dart
  • 连接器测试: todo_screen_CONNECTOR_test.dart
  • 表示测试: todo_screen_PRESENTATION_test.dart

连接器测试专注于视图模型逻辑 - 验证属性正确从状态派生,回调分派适当动作。

参考

文档中的URL: