Claude-skill-registry asyncredux-testing-basics

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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/asyncredux-testing-basics" ~/.claude/skills/majiayu000-claude-skill-registry-asyncredux-testing-basics && rm -rf "$T"
manifest: skills/data/asyncredux-testing-basics/SKILL.md
source content

Testing AsyncRedux Actions

The recommended approach for testing AsyncRedux is to use the

Store
directly rather than the deprecated
StoreTester
. This provides a clean, straightforward testing pattern.

Creating a Test Store

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
}

Basic Test Pattern: Dispatch, Wait, Expect

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');
});

Testing Async Actions

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');
});

Testing Multiple Actions in Parallel

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);
});

Verifying Action Errors with ActionStatus

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);
});

ActionStatus Properties

  • isCompleted
    : Whether the action finished executing
  • isCompletedOk
    : True if action finished without errors (both
    before()
    and
    reduce()
    completed successfully)
  • isCompletedFailed
    : True if action threw an error
  • originalError
    : The error thrown by
    before()
    or
    reduce()
  • wrappedError
    : The error after
    wrapError()
    processing
  • hasFinishedMethodBefore
    : Whether
    before()
    completed
  • hasFinishedMethodReduce
    : Whether
    reduce()
    completed
  • hasFinishedMethodAfter
    : Whether
    after()
    completed

Testing UserException Errors

Test 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.');
});

Testing Multiple Errors with Error Queue

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);
});

Conditional Navigation After Action Success

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);
});

Testing State Unchanged on Error

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);
});

Using MockStore for Dependency Isolation

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');
});

Advanced Wait Methods for Complex Tests

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([]);

Test File Organization

Recommended naming convention for test files:

  • Widget:
    my_feature.dart
  • State tests:
    my_feature_STATE_test.dart
  • Connector tests:
    my_feature_CONNECTOR_test.dart
  • Presentation tests:
    my_feature_PRESENTATION_test.dart

Complete Test Example

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); // State unchanged
    });
  });
}

References

URLs from the documentation: