名称: asyncredux-testing-wait-methods
描述: 使用高级等待方法处理复杂测试场景。覆盖 waitCondition()、waitAllActions()、waitActionType()、waitAllActionTypes()、waitAnyActionTypeFinishes() 以及 completeImmediately 参数。
测试高级等待方法
在 AsyncRedux 中测试复杂异步场景时,基本的 dispatchAndWait() 可能不足够。存储提供了几种高级等待方法,用于精细控制测试何时进行。
等待方法概述
| 方法 | 目的 |
|---|---|
waitCondition() |
等待直到状态满足条件 |
waitAllActions() |
等待特定动作完成,或直到没有动作在进行中 |
waitActionType() |
等待直到给定类型的动作不在进行中 |
waitAllActionTypes() |
等待直到给定类型的所有动作不在进行中 |
waitAnyActionTypeFinishes() |
等待直到给定类型的任何动作完成 |
waitActionCondition() |
低级:等待直到进行中的动作满足自定义条件 |
waitCondition()
等待直到状态满足给定条件。返回触发状态变化的动作。
Future<ReduxAction<St>?> waitCondition(
bool Function(St) condition, {
bool completeImmediately = true, // 注意:这里默认是 TRUE
int? timeoutMillis,
})
基本用法
test('waitCondition 等待状态匹配', () async {
var store = Store<AppState>(initialState: AppState(count: 1));
// 分发一个将改变状态的异步动作
store.dispatch(IncrementActionAsync());
// 等待直到计数变为 2
var action = await store.waitCondition((state) => state.count == 2);
expect(store.state.count, 2);
expect(action, isA<IncrementActionAsync>());
});
条件已为真
默认情况下,如果条件已为真,未来立即完成:
test('当条件已为真时立即完成', () async {
var store = Store<AppState>(initialState: AppState(count: 5));
// 条件已为真 - 立即完成
await store.waitCondition((state) => state.count == 5);
expect(store.state.count, 5);
});
使用 completeImmediately: false
要求条件必须变为真(而不是已经为真):
test('当条件已为真且 completeImmediately: false 时抛出异常', () async {
var store = Store<AppState>(initialState: AppState(count: 1));
// 这将抛出因为条件已为真
expect(
() => store.waitCondition(
(state) => state.count == 1,
completeImmediately: false,
),
throwsA(isA<StoreException>()),
);
});
waitAllActions()
等待特定动作完成,或等待直到没有动作在进行中(当传递空列表或 null 时)。
Future<void> waitAllActions(
List<ReduxAction<St>>? actions, {
bool completeImmediately = false, // 注意:这里默认是 FALSE
int? timeoutMillis,
})
等待所有动作完成
test('waitAllActions 等待所有分发的动作', () async {
var store = Store<AppState>(initialState: AppState(count: 1));
var action1 = DelayedIncrementAction(10, delayMillis: 50);
var action2 = DelayedIncrementAction(100, delayMillis: 100);
var action3 = DelayedIncrementAction(1000, delayMillis: 20);
// 并行分发动作
store.dispatch(action1);
store.dispatch(action2);
store.dispatch(action3);
expect(store.state.count, 1); // 尚未改变
// 等待所有三个动作完成
await store.waitAllActions([action1, action2, action3]);
expect(store.state.count, 1 + 10 + 100 + 1000);
});
等待直到没有动作在进行中
传递空列表或 null 以等待直到没有动作在运行:
test('waitAllActions 使用空列表等待所有完成', () async {
var store = Store<AppState>(initialState: AppState(count: 1));
store.dispatch(DelayedAction(10, delayMillis: 50));
store.dispatch(DelayedAction(100, delayMillis: 100));
store.dispatch(DelayedAction(1000, delayMillis: 20));
expect(store.state.count, 1);
// 等待直到所有动作完成(没有动作在进行中)
await store.waitAllActions([]);
expect(store.state.count, 1 + 10 + 100 + 1000);
});
选择性等待
只等待某些动作完成,忽略其他:
test('只等待特定动作', () async {
var store = Store<AppState>(initialState: AppState(count: 1));
var action50 = DelayedAction(10, delayMillis: 50);
var action100 = AnotherDelayedAction(100, delayMillis: 100);
var action200 = SlowAction(100000, delayMillis: 200); // 非常慢
var action10 = DelayedAction(1000, delayMillis: 10);
store.dispatch(action50);
store.dispatch(action100);
store.dispatch(action200); // 我们不等这一个
store.dispatch(action10);
// 只等待快速动作
await store.waitAllActions([action50, action100, action10]);
// 慢动作尚未完成
expect(store.state.count, 1 + 10 + 100 + 1000);
});
waitActionType()
等待直到给定类型的动作不在进行中。返回完成的动作(或如果没有动作在进行中则返回 null)。
Future<ReduxAction<St>?> waitActionType(
Type actionType, {
bool completeImmediately = false,
int? timeoutMillis,
})
基本用法
test('waitActionType 等待动作类型完成', () async {
var store = Store<AppState>(initialState: AppState(count: 1));
store.dispatch(DelayedAction(1000, delayMillis: 10));
expect(store.state.count, 1);
// 等待任何 DelayedAction 完成
var action = await store.waitActionType(DelayedAction);
expect(store.state.count, 1001);
expect(action, isA<DelayedAction>());
});
检查动作状态
test('可以检查完成动作的状态', () async {
var store = Store<AppState>(initialState: AppState(count: 1));
store.dispatch(ActionThatMayFail());
var action = await store.waitActionType(ActionThatMayFail);
expect(action?.status.isCompletedOk, isTrue);
// 或检查错误:
// expect(action?.status.originalError, isA<UserException>());
});
顺序等待多个类型
test('等待多个动作类型', () async {
var store = Store<AppState>(initialState: AppState(count: 1));
store.dispatch(AnotherDelayedAction(123, delayMillis: 100));
store.dispatch(DelayedAction(1000, delayMillis: 10));
expect(store.state.count, 1);
// DelayedAction 先完成(10ms)
await store.waitActionType(DelayedAction);
expect(store.state.count, 1001);
// AnotherDelayedAction 后完成(100ms)
await store.waitActionType(AnotherDelayedAction);
expect(store.state.count, 1124);
});
waitAllActionTypes()
等待直到给定类型的所有动作不在进行中。
Future<void> waitAllActionTypes(
List<Type> actionTypes, {
bool completeImmediately = false,
int? timeoutMillis,
})
基本用法
test('waitAllActionTypes 等待所有类型', () async {
var store = Store<AppState>(initialState: AppState(count: 1));
store.dispatch(DelayedAction(10, delayMillis: 50));
store.dispatch(AnotherDelayedAction(100, delayMillis: 100));
store.dispatch(SlowAction(100000, delayMillis: 200));
store.dispatch(DelayedAction(1000, delayMillis: 10));
expect(store.state.count, 1);
// 只等待 DelayedAction 和 AnotherDelayedAction 类型
await store.waitAllActionTypes([DelayedAction, AnotherDelayedAction]);
// SlowAction 尚未完成(200ms),但我们没等它
expect(store.state.count, 1 + 10 + 100 + 1000);
});
waitAnyActionTypeFinishes()
重要: 此方法与其他方法不同。它等待直到给定类型的任何动作完成分发,即使这些动作在方法调用时不在进行中。
Future<ReduxAction<St>> waitAnyActionTypeFinishes(
List<Type> actionTypes, {
int? timeoutMillis,
})
用例:等待嵌套动作
这在动作内部分发其他动作时很有用,您想等待其中一个嵌套动作完成:
test('waitAnyActionTypeFinishes 等待嵌套动作', () async {
var store = Store<AppState>(initialState: AppState(count: 1));
// StartAction 内部分发 DelayedAction
store.dispatch(StartAction());
// 等待 DelayedAction 完成(即使它尚未分发)
var action = await store.waitAnyActionTypeFinishes([DelayedAction]);
expect(action, isA<DelayedAction>());
expect(action.status.isCompletedOk, isTrue);
});
多个类型 - 第一个完成的
test('返回第一个完成的动作类型', () async {
var store = Store<AppState>(initialState: AppState());
store.dispatch(ProcessStocksAction()); // 分发 BuyAction 或 SellAction
// 等待 BuyAction 或 SellAction 完成
var action = await store.waitAnyActionTypeFinishes([BuyAction, SellAction]);
expect(action.runtimeType, anyOf(equals(BuyAction), equals(SellAction)));
});
waitActionCondition()
低级方法,等待直到进行中的动作集满足自定义条件。这是其他等待方法内部使用的方法。
Future<(Set<ReduxAction<St>>, ReduxAction<St>?)> waitActionCondition(
bool Function(Set<ReduxAction<St>> actions, ReduxAction<St>? triggerAction) condition, {
bool completeImmediately = false,
String completedErrorMessage = "等待的动作条件已为真",
int? timeoutMillis,
})
示例:自定义条件
test('waitActionCondition 使用自定义条件', () async {
var store = Store<AppState>(initialState: AppState(count: 1));
// 等待直到没有动作在进行中
await store.waitActionCondition(
(actions, triggerAction) => actions.isEmpty,
completeImmediately: true,
);
});
completeImmediately 参数
此参数控制当方法调用时条件已满足时的行为:
| 方法 | 默认 | 当 true |
当 false |
|---|---|---|---|
waitCondition |
true |
立即完成 | 抛出 StoreException |
waitAllActions |
false |
立即完成 | 抛出 StoreException |
waitActionType |
false |
立即完成,返回 null |
抛出 StoreException |
waitAllActionTypes |
false |
立即完成 | 抛出 StoreException |
waitActionCondition |
false |
立即完成 | 抛出 StoreException |
注意: waitCondition 默认是 true,因为它常用于检查“状态是否就绪?”,在这种情况下,如果已经就绪,您想继续。其他方法默认是 false,因为它们通常用于等待应该在进行的动作。
test('completeImmediately 行为', () async {
var store = Store<AppState>(initialState: AppState(count: 1));
// waitCondition:completeImmediately 默认是 TRUE
await store.waitCondition((state) => state.count == 1); // OK,完成
// waitAllActions:completeImmediately 默认是 FALSE
expect(
() => store.waitAllActions([]), // 没有动作在进行中
throwsA(isA<StoreException>()),
);
// 使用 completeImmediately: true 来允许
await store.waitAllActions([], completeImmediately: true); // OK
});
超时配置
所有等待方法都支持 timeoutMillis 参数。默认超时是 10 分钟。
test('waitCondition 带超时', () async {
var store = Store<AppState>(initialState: AppState(count: 1));
// 这个条件永远不会为真,所以会超时
expect(
() => store.waitCondition(
(state) => state.count == 999,
timeoutMillis: 10, // 10ms 超时
),
throwsA(isA<TimeoutException>()),
);
});
全局超时配置
修改 Store.defaultTimeoutMillis 以更改所有等待方法的默认值:
void main() {
// 设置全局默认超时为 30 秒
Store.defaultTimeoutMillis = 30 * 1000;
// 要完全禁用超时,使用 -1
Store.defaultTimeoutMillis = -1;
}
完整测试示例
import 'dart:async';
import 'package:async_redux/async_redux.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('等待方法', () {
test('waitCondition 等待状态变化', () async {
var store = Store<State>(initialState: State(1));
// 分发异步动作
store.dispatch(IncrementActionAsync());
// 等待状态改变
await store.waitCondition((state) => state.count == 2);
expect(store.state.count, 2);
});
test('waitAllActions 等待所有动作', () async {
var store = Store<State>(initialState: State(1));
store.dispatch(DelayedAction(10, delayMillis: 50));
store.dispatch(DelayedAction(100, delayMillis: 100));
store.dispatch(DelayedAction(1000, delayMillis: 20));
await store.waitAllActions([]);
expect(store.state.count, 1111);
});
test('waitActionType 等待特定类型', () async {
var store = Store<State>(initialState: State(1));
store.dispatch(DelayedAction(1000, delayMillis: 10));
var action = await store.waitActionType(DelayedAction);
expect(store.state.count, 1001);
expect(action?.status.isCompletedOk, isTrue);
});
test('waitAllActionTypes 等待多个类型', () async {
var store = Store<State>(initialState: State(1));
store.dispatch(DelayedAction(10, delayMillis: 50));
store.dispatch(AnotherAction(100, delayMillis: 100));
await store.waitAllActionTypes([DelayedAction, AnotherAction]);
expect(store.state.count, 111);
});
test('waitAnyActionTypeFinishes 等待第一个完成', () async {
var store = Store<State>(initialState: State(1));
store.dispatch(DelayedAction(1, delayMillis: 10));
var action = await store.waitAnyActionTypeFinishes([DelayedAction]);
expect(action, isA<DelayedAction>());
expect(action.status.isCompletedOk, isTrue);
});
});
}
// 测试状态和动作
class State {
final int count;
State(this.count);
}
class IncrementActionAsync extends ReduxAction<State> {
@override
Future<State> reduce() async {
await Future.delayed(Duration(milliseconds: 10));
return State(state.count + 1);
}
}
class DelayedAction extends ReduxAction<State> {
final int increment;
final int delayMillis;
DelayedAction(this.increment, {required this.delayMillis});
@override
Future<State> reduce() async {
await Future.delayed(Duration(milliseconds: delayMillis));
return State(state.count + increment);
}
}
class AnotherAction extends DelayedAction {
AnotherAction(int increment, {required int delayMillis})
: super(increment, delayMillis: delayMillis);
}
参考
文档中的 URL:
- https://asyncredux.com/flutter/testing/dispatch-wait-and-expect
- https://asyncredux.com/flutter/testing/store-tester
- https://asyncredux.com/flutter/miscellaneous/wait-condition
- https://asyncredux.com/flutter/miscellaneous/advanced-waiting
- https://asyncredux.com/flutter/testing/mocking
- https://asyncredux.com/flutter/basics/dispatching-actions