AsyncRedux测试基础Skill asyncredux-testing-basics

这个技能是关于如何使用AsyncRedux库中的Store直接编写单元测试,覆盖创建测试存储、使用dispatchAndWait()方法、检查动作后状态变化、验证动作错误、测试异步动作等。适用于Flutter/Dart应用的异步状态管理测试,关键词包括AsyncRedux、单元测试、Flutter、Dart、异步动作、错误处理、ActionStatus。

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

name: asyncredux-testing-basics description: 直接使用Store编写AsyncRedux动作的单元测试。涵盖了创建具有初始状态的测试存储、使用 dispatchAndWait()、检查动作后状态、通过ActionStatus验证动作错误,以及测试异步动作。

测试AsyncRedux动作

推荐使用 Store 直接测试AsyncRedux,而不是已弃用的 StoreTester。这提供了一种干净、直接的测试模式。

创建测试存储

创建具有测试专用初始状态的存储:

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

void main() {
  test('should increment counter', () async {
    // 创建具有初始状态的存储
    var store = Store<AppState>(
      initialState: AppState(counter: 0, name: ''),
    );

    // 在此测试您的动作
  });
}

为了测试隔离,在每个测试中创建一个新的存储:

void main() {
  late Store<AppState> store;

  setUp(() {
    store = Store<AppState>(
      initialState: AppState.initialState(),
    );
  });

  tearDown(() {
    store.shutdown();
  });

  // 测试在此处进行
}

基本测试模式:分发、等待、期望

使用 dispatchAndWait() 分发动作并等待其完成:

test('SaveNameAction updates the name', () async {
  var store = Store<AppState>(
    initialState: AppState(name: ''),
  );

  await store.dispatchAndWait(SaveNameAction('John'));

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

测试异步动作

异步动作的工作方式相同—— dispatchAndWait() 仅在动作完全完成时返回:

class FetchUserAction extends ReduxAction<AppState> {
  final String userId;
  FetchUserAction(this.userId);

  Future<AppState?> reduce() async {
    var user = await api.fetchUser(userId);
    return state.copy(user: user);
  }
}

test('FetchUserAction loads user data', () async {
  var store = Store<AppState>(
    initialState: AppState(user: null),
  );

  await store.dispatchAndWait(FetchUserAction('123'));

  expect(store.state.user, isNotNull);
  expect(store.state.user!.id, '123');
});

测试并行多个动作

使用 dispatchAndWaitAll() 分发多个动作并等待所有完成:

test('can buy and sell stocks in parallel', () async {
  var store = Store<AppState>(
    initialState: AppState(portfolio: Portfolio.empty()),
  );

  await store.dispatchAndWaitAll([
    BuyAction('IBM', quantity: 10),
    SellAction('TSLA', quantity: 5),
  ]);

  expect(store.state.portfolio.holdings['IBM'], 10);
  expect(store.state.portfolio.holdings['TSLA'], isNull);
});

使用ActionStatus验证动作错误

dispatchAndWait() 返回一个 ActionStatus 对象,可让您验证动作是否成功或失败:

test('SaveAction fails with invalid data', () async {
  var store = Store<AppState>(
    initialState: AppState.initialState(),
  );

  var status = await store.dispatchAndWait(SaveAction(amount: -100));

  expect(status.isCompletedFailed, isTrue);
  expect(status.isCompletedOk, isFalse);
});

ActionStatus属性

  • isCompleted:动作是否执行完成
  • isCompletedOk:如果动作完成时没有错误(包括 before()reduce() 成功完成)则为真
  • isCompletedFailed:如果动作抛出错误则为真
  • originalError:由 before()reduce() 抛出的错误
  • wrappedError:经过 wrapError() 处理后的错误
  • hasFinishedMethodBeforebefore() 是否完成
  • hasFinishedMethodReducereduce() 是否完成
  • hasFinishedMethodAfterafter() 是否完成

测试UserException错误

测试动作是否抛出适当的 UserException 错误:

class TransferMoney extends ReduxAction<AppState> {
  final double amount;
  TransferMoney(this.amount);

  AppState? reduce() {
    if (amount <= 0) {
      throw UserException('Amount must be positive.');
    }
    return state.copy(balance: state.balance - amount);
  }
}

test('TransferMoney throws UserException for invalid amount', () async {
  var store = Store<AppState>(
    initialState: AppState(balance: 1000),
  );

  var status = await store.dispatchAndWait(TransferMoney(0));

  expect(status.isCompletedFailed, isTrue);

  var error = status.wrappedError;
  expect(error, isA<UserException>());
  expect((error as UserException).msg, 'Amount must be positive.');
});

使用错误队列测试多个错误

当多个动作失败时,检查存储的错误队列:

test('multiple actions can fail', () async {
  var store = Store<AppState>(
    initialState: AppState.initialState(),
  );

  await store.dispatchAndWaitAll([
    InvalidAction1(),
    InvalidAction2(),
  ]);

  // 检查存储错误队列中的错误
  expect(store.errors.length, 2);
});

动作成功后的条件导航

常见模式是在动作成功后进行导航:

test('navigate only on successful save', () async {
  var store = Store<AppState>(
    initialState: AppState.initialState(),
  );

  var status = await store.dispatchAndWait(SaveAction(data: validData));

  expect(status.isCompletedOk, isTrue);
  // 在实际代码中: if (status.isCompletedOk) Navigator.pop(context);
});

测试错误时状态不变

当动作抛出错误时,状态应保持不变:

test('state unchanged when action fails', () async {
  var store = Store<AppState>(
    initialState: AppState(counter: 5),
  );

  var initialState = store.state;

  await store.dispatchAndWait(FailingAction());

  // 状态不应改变
  expect(store.state.counter, 5);
  expect(store.state, initialState);
});

使用MockStore进行依赖隔离

使用 MockStore 在测试中模拟特定动作:

test('with mocked dependency action', () async {
  var store = MockStore<AppState>(
    initialState: AppState.initialState(),
    mocks: {
      // 禁用该动作(不运行)
      FetchFromServerAction: null,

      // 或用自定义状态修改替换
      FetchFromServerAction: (action, state) =>
        state.copy(data: 'mocked data'),
    },
  );

  await store.dispatchAndWait(ActionThatDependsOnFetch());

  expect(store.state.data, 'mocked data');
});

复杂测试的高级等待方法

对于复杂的异步场景,使用这些额外的等待方法:

// 等待特定状态条件
await store.waitCondition((state) => state.isLoaded);

// 等待所有给定动作类型完成
await store.waitAllActionTypes([LoadAction, ProcessAction]);

// 等待任何给定动作类型的动作完成
await store.waitAnyActionTypeFinishes([LoadAction]);

// 等待直到没有动作在进行中
await store.waitAllActions([]);

测试文件组织

推荐测试文件命名约定:

  • Widget: my_feature.dart
  • 状态测试: my_feature_STATE_test.dart
  • 连接器测试: my_feature_CONNECTOR_test.dart
  • 表示层测试: my_feature_PRESENTATION_test.dart

完整测试示例

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

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

    setUp(() {
      store = Store<AppState>(
        initialState: AppState(counter: 0),
      );
    });

    test('increments counter by 1', () async {
      await store.dispatchAndWait(IncrementAction());
      expect(store.state.counter, 1);
    });

    test('increments counter multiple times', () async {
      await store.dispatchAndWait(IncrementAction());
      await store.dispatchAndWait(IncrementAction());
      await store.dispatchAndWait(IncrementAction());
      expect(store.state.counter, 3);
    });

    test('handles concurrent increments', () async {
      await store.dispatchAndWaitAll([
        IncrementAction(),
        IncrementAction(),
        IncrementAction(),
      ]);
      expect(store.state.counter, 3);
    });
  });

  group('FetchDataAction', () {
    test('succeeds with valid response', () async {
      var store = Store<AppState>(
        initialState: AppState(data: null),
      );

      var status = await store.dispatchAndWait(FetchDataAction());

      expect(status.isCompletedOk, isTrue);
      expect(store.state.data, isNotNull);
    });

    test('fails gracefully on error', () async {
      var store = Store<AppState>(
        initialState: AppState(data: null),
      );

      var status = await store.dispatchAndWait(
        FetchDataAction(simulateError: true),
      );

      expect(status.isCompletedFailed, isTrue);
      expect(status.wrappedError, isA<UserException>());
      expect(store.state.data, isNull); // 状态未改变
    });
  });
}

参考文献

文档中的URL: