Test StoreConnector view-models in isolation. Covers creating view-models with `Vm.createFrom()`, testing view-model properties, testing callbacks that dispatch actions, and verifying state changes from callbacks.
View-models created by VmFactory can be tested in isolation without building widgets. Use Vm.createFrom() to instantiate the view-model directly, then verify properties and execute callbacks.
Use Vm.createFrom() with a store and factory instance:
import 'package:flutter_test/flutter_test.dart';
import 'package:async_redux/async_redux.dart';
test('view-model has correct properties', () {
var store = Store<AppState>(
initialState: AppState(name: 'Mary', counter: 5),
);
var vm = Vm.createFrom(store, CounterFactory());
expect(vm.counter, 5);
expect(vm.name, 'Mary');
});
Important: Vm.createFrom() can only be called once per factory instance. Create a new factory for each test.
Verify that the factory correctly transforms state into view-model properties:
class CounterViewModel extends Vm {
final int counter;
final String description;
final VoidCallback onIncrement;
CounterViewModel({
required this.counter,
required this.description,
required this.onIncrement,
}) : super(equals: [counter, description]);
}
class CounterFactory extends VmFactory<AppState, CounterConnector, CounterViewModel> {
@override
CounterViewModel fromStore() => CounterViewModel(
counter: state.counter,
description: 'Count is ${state.counter}',
onIncrement: () => dispatch(IncrementAction()),
);
}
test('factory transforms state correctly', () {
var store = Store<AppState>(
initialState: AppState(counter: 10),
);
var vm = Vm.createFrom(store, CounterFactory());
expect(vm.counter, 10);
expect(vm.description, 'Count is 10');
});
When testing callbacks, invoke them and then use wait methods to verify actions were dispatched and state changed:
test('onIncrement dispatches IncrementAction', () async {
var store = Store<AppState>(
initialState: AppState(counter: 0),
);
var vm = Vm.createFrom(store, CounterFactory());
// Invoke the callback
vm.onIncrement();
// Wait for the action to complete
await store.waitActionType(IncrementAction);
// Verify state changed
expect(store.state.counter, 1);
});
Several wait methods help verify callback behavior:
Wait for a specific action type to finish:
test('callback dispatches expected action', () async {
var store = Store<AppState>(initialState: AppState(name: ''));
var vm = Vm.createFrom(store, UserFactory());
vm.onSave('John');
await store.waitActionType(SaveNameAction);
expect(store.state.name, 'John');
});
Wait for multiple action types to complete:
test('callback triggers multiple actions', () async {
var store = Store<AppState>(initialState: AppState.initialState());
var vm = Vm.createFrom(store, CheckoutFactory());
vm.onCheckout();
await store.waitAllActionTypes([ValidateCartAction, ProcessPaymentAction]);
expect(store.state.orderCompleted, isTrue);
});
Wait for any matching action to finish, useful when testing actions that may or may not be dispatched:
test('refresh triggers data fetch', () async {
var store = Store<AppState>(initialState: AppState.initialState());
var vm = Vm.createFrom(store, DataFactory());
vm.onRefresh();
var action = await store.waitAnyActionTypeFinishes([FetchDataAction]);
expect(action, isA<FetchDataAction>());
expect(store.state.data, isNotEmpty);
});
Wait for state to meet a specific condition:
test('loading completes when data is fetched', () async {
var store = Store<AppState>(initialState: AppState(isLoading: false, data: null));
var vm = Vm.createFrom(store, DataFactory());
vm.onLoad();
await store.waitCondition((state) => state.data != null);
expect(store.state.isLoading, isFalse);
expect(store.state.data, isNotNull);
});
Wait until no actions are in progress:
test('all actions complete', () async {
var store = Store<AppState>(initialState: AppState.initialState());
var vm = Vm.createFrom(store, BatchFactory());
vm.onProcessBatch();
await store.waitAllActions([]);
expect(store.state.batchProcessed, isTrue);
});
Verify that callbacks dispatch actions that succeed or fail appropriately:
test('save callback handles errors', () async {
var store = Store<AppState>(
initialState: AppState(data: ''),
);
var vm = Vm.createFrom(store, FormFactory());
// Trigger save with invalid data
vm.onSave('');
// dispatchAndWait returns ActionStatus, but when testing callbacks,
// use waitActionType and check store.errors
await store.waitActionType(SaveAction);
// Check if action failed
expect(store.errors, isNotEmpty);
});
Async callbacks work the same way - wait for the dispatched actions:
class UserFactory extends VmFactory<AppState, UserConnector, UserViewModel> {
@override
UserViewModel fromStore() => UserViewModel(
user: state.user,
onRefresh: () => dispatch(FetchUserAction()),
);
}
test('onRefresh loads user data', () async {
var store = Store<AppState>(
initialState: AppState(user: null),
);
var vm = Vm.createFrom(store, UserFactory());
vm.onRefresh();
await store.waitActionType(FetchUserAction);
expect(store.state.user, isNotNull);
});
Use MockStore to mock actions triggered by callbacks:
test('callback with mocked dependency', () async {
var store = MockStore<AppState>(
initialState: AppState(data: null),
mocks: {
// Mock the API call to return test data
FetchDataAction: (action, state) => state.copy(data: 'mocked data'),
},
);
var vm = Vm.createFrom(store, DataFactory());
vm.onFetch();
await store.waitActionType(FetchDataAction);
expect(store.state.data, 'mocked data');
});
Use ConnectorTester to test lifecycle callbacks without building widgets:
class MyScreen extends StatelessWidget {
@override
Widget build(BuildContext context) => StoreConnector<AppState, MyViewModel>(
vm: () => MyFactory(),
onInit: (store) => store.dispatch(StartPollingAction()),
onDispose: (store) => store.dispatch(StopPollingAction()),
builder: (context, vm) => MyWidget(vm: vm),
);
}
test('onInit dispatches StartPollingAction', () async {
var store = Store<AppState>(initialState: AppState.initialState());
var connectorTester = store.getConnectorTester(MyScreen());
connectorTester.runOnInit();
var action = await store.waitAnyActionTypeFinishes([StartPollingAction]);
expect(action, isA<StartPollingAction>());
});
test('onDispose dispatches StopPollingAction', () async {
var store = Store<AppState>(initialState: AppState.initialState());
var connectorTester = store.getConnectorTester(MyScreen());
connectorTester.runOnDispose();
var action = await store.waitAnyActionTypeFinishes([StopPollingAction]);
expect(action, isA<StopPollingAction>());
});
import 'package:flutter_test/flutter_test.dart';
import 'package:async_redux/async_redux.dart';
// View-Model
class TodoViewModel extends Vm {
final List<String> todos;
final bool isLoading;
final void Function(String) onAddTodo;
final void Function(int) onRemoveTodo;
final VoidCallback onRefresh;
TodoViewModel({
required this.todos,
required this.isLoading,
required this.onAddTodo,
required this.onRemoveTodo,
required this.onRefresh,
}) : super(equals: [todos, isLoading]);
}
// Factory
class TodoFactory extends VmFactory<AppState, TodoConnector, TodoViewModel> {
@override
TodoViewModel fromStore() => TodoViewModel(
todos: state.todos,
isLoading: state.isLoading,
onAddTodo: (text) => dispatch(AddTodoAction(text)),
onRemoveTodo: (index) => dispatch(RemoveTodoAction(index)),
onRefresh: () => dispatch(FetchTodosAction()),
);
}
void main() {
group('TodoFactory', () {
late Store<AppState> store;
setUp(() {
store = Store<AppState>(
initialState: AppState(todos: [], isLoading: false),
);
});
test('creates view-model with correct initial properties', () {
var vm = Vm.createFrom(store, TodoFactory());
expect(vm.todos, isEmpty);
expect(vm.isLoading, isFalse);
});
test('onAddTodo dispatches AddTodoAction', () async {
var vm = Vm.createFrom(store, TodoFactory());
vm.onAddTodo('Buy milk');
await store.waitActionType(AddTodoAction);
expect(store.state.todos, contains('Buy milk'));
});
test('onRemoveTodo dispatches RemoveTodoAction', () async {
store = Store<AppState>(
initialState: AppState(todos: ['Task 1', 'Task 2'], isLoading: false),
);
var vm = Vm.createFrom(store, TodoFactory());
vm.onRemoveTodo(0);
await store.waitActionType(RemoveTodoAction);
expect(store.state.todos, ['Task 2']);
});
test('onRefresh fetches todos', () async {
var vm = Vm.createFrom(store, TodoFactory());
vm.onRefresh();
await store.waitCondition((state) => !state.isLoading);
expect(store.state.todos, isNotEmpty);
});
});
}
Follow the recommended naming convention for test files:
todo_screen.darttodo_screen_connector.darttodo_screen_STATE_test.darttodo_screen_CONNECTOR_test.darttodo_screen_PRESENTATION_test.dartConnector tests focus on view-model logic - verifying properties are correctly derived from state and callbacks dispatch appropriate actions.
URLs from the documentation:
npx skills add marcglasberg/asyncredux-testing-view-models下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer