.claude/skills/asyncredux-testing-basics/SKILL.md
Write unit tests for AsyncRedux actions using the Store directly. Covers creating test stores with initial state, using `dispatchAndWait()`, checking state after actions, verifying action errors via ActionStatus, and testing async actions.
npx skillsauth add marcglasberg/async_redux asyncredux-testing-basicsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The recommended approach for testing AsyncRedux is to use the Store directly rather than the deprecated StoreTester. This provides a clean, straightforward testing pattern.
Create a store with test-specific initial state:
import 'package:flutter_test/flutter_test.dart';
import 'package:async_redux/async_redux.dart';
void main() {
test('should increment counter', () async {
// Create store with initial state
var store = Store<AppState>(
initialState: AppState(counter: 0, name: ''),
);
// Test your actions here
});
}
For test isolation, create a fresh store in each test:
void main() {
late Store<AppState> store;
setUp(() {
store = Store<AppState>(
initialState: AppState.initialState(),
);
});
tearDown(() {
store.shutdown();
});
// Tests go here
}
Use dispatchAndWait() to dispatch an action and wait for it to complete:
test('SaveNameAction updates the name', () async {
var store = Store<AppState>(
initialState: AppState(name: ''),
);
await store.dispatchAndWait(SaveNameAction('John'));
expect(store.state.name, 'John');
});
Async actions work the same way - dispatchAndWait() returns only when the action fully completes:
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');
});
Use dispatchAndWaitAll() to dispatch multiple actions and wait for all to complete:
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);
});
dispatchAndWait() returns an ActionStatus object that lets you verify if an action succeeded or failed:
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);
});
isCompleted: Whether the action finished executingisCompletedOk: True if action finished without errors (both before() and reduce() completed successfully)isCompletedFailed: True if action threw an errororiginalError: The error thrown by before() or reduce()wrappedError: The error after wrapError() processinghasFinishedMethodBefore: Whether before() completedhasFinishedMethodReduce: Whether reduce() completedhasFinishedMethodAfter: Whether after() completedTest that actions throw appropriate UserException errors:
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.');
});
When multiple actions fail, check the store's error queue:
test('multiple actions can fail', () async {
var store = Store<AppState>(
initialState: AppState.initialState(),
);
await store.dispatchAndWaitAll([
InvalidAction1(),
InvalidAction2(),
]);
// Check errors in the store's error queue
expect(store.errors.length, 2);
});
A common pattern is navigating only after an action succeeds:
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);
// In real code: if (status.isCompletedOk) Navigator.pop(context);
});
When an action throws, state should remain unchanged:
test('state unchanged when action fails', () async {
var store = Store<AppState>(
initialState: AppState(counter: 5),
);
var initialState = store.state;
await store.dispatchAndWait(FailingAction());
// State should not have changed
expect(store.state.counter, 5);
expect(store.state, initialState);
});
Use MockStore to mock specific actions in tests:
test('with mocked dependency action', () async {
var store = MockStore<AppState>(
initialState: AppState.initialState(),
mocks: {
// Disable the action (don't run it)
FetchFromServerAction: null,
// Or replace with custom state modification
FetchFromServerAction: (action, state) =>
state.copy(data: 'mocked data'),
},
);
await store.dispatchAndWait(ActionThatDependsOnFetch());
expect(store.state.data, 'mocked data');
});
For complex async scenarios, use these additional wait methods:
// Wait for a specific state condition
await store.waitCondition((state) => state.isLoaded);
// Wait for all given action types to complete
await store.waitAllActionTypes([LoadAction, ProcessAction]);
// Wait for any action of given types to finish
await store.waitAnyActionTypeFinishes([LoadAction]);
// Wait until no actions are in progress
await store.waitAllActions([]);
Recommended naming convention for test files:
my_feature.dartmy_feature_STATE_test.dartmy_feature_CONNECTOR_test.dartmy_feature_PRESENTATION_test.dartimport '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); // State unchanged
});
});
}
URLs from the documentation:
data-ai
Show loading states and handle action failures in widgets. Covers `isWaiting(ActionType)` for spinners, `isFailed(ActionType)` for error states, `exceptionFor(ActionType)` for error messages, and `clearExceptionFor()` to reset failure states.
data-ai
Use `waitCondition()` inside actions to pause execution until state meets criteria. Covers waiting for price thresholds, coordinating between actions, and implementing conditional workflows.
testing
Handle user-facing errors with UserException. Covers throwing UserException from actions, setting up UserExceptionDialog, customizing error dialogs with `onShowUserExceptionDialog`, and using UserExceptionAction for non-interrupting error display.
tools
Implement undo/redo functionality using state observers. Covers recording state history with stateObserver, creating a RecoverStateAction, implementing undo for the full state or partial state, and managing history limits.