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()处理后的错误hasFinishedMethodBefore:before()是否完成hasFinishedMethodReduce:reduce()是否完成hasFinishedMethodAfter:after()是否完成
测试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:
- https://asyncredux.com/flutter/testing/store-tester
- https://asyncredux.com/flutter/testing/dispatch-wait-and-expect
- https://asyncredux.com/flutter/testing/test-files
- https://asyncredux.com/flutter/testing/mocking
- https://asyncredux.com/flutter/testing/testing-user-exceptions
- https://asyncredux.com/flutter/advanced-actions/action-status
- https://asyncredux.com/flutter/basics/async-actions
- https://asyncredux.com/flutter/basics/dispatching-actions
- https://asyncredux.com/flutter/basics/failed-actions
- https://asyncredux.com/flutter/advanced-actions/errors-thrown-by-actions
- https://asyncredux.com/flutter/basics/store
- https://asyncredux.com/flutter/miscellaneous/advanced-waiting