Skip to content

Commit

Permalink
More test methods in the store. Test the view model.
Browse files Browse the repository at this point in the history
  • Loading branch information
marcglasberg committed Mar 8, 2024
1 parent 0dbf906 commit 897d656
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 82 deletions.
69 changes: 63 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ an <a href="https://github.com/marcglasberg/SameAppDifferentTech/blob/main/Mobil
Async Redux App Example Repository</a> in GitHub for a full-fledged example with a complete app
showcasing the fundamentals and best practices described in the AsyncRedux README.md file._

# 22.0.0-dev.3
# 22.0.0-dev.6

* Some features of the async_redux package are now available as a standalone Dart-only
package: https://pub.dev/packages/async_redux_core. You may use that core package when you
Expand All @@ -19,6 +19,7 @@ showcasing the fundamentals and best practices described in the AsyncRedux READM
> You should continue to use this async_redux package, which already exports
> the code that's now in the core package.

* BREAKING CHANGE: The `UserException` class was modified so that it was possible to move it to the
`async_redux_core`. If your use of `UserException` was limited to specifying the error message,
then you don't need to change anything: `throw UserException('Error message')` will continue to
Expand All @@ -44,6 +45,67 @@ showcasing the fundamentals and best practices described in the AsyncRedux READM
language by using the [i18n_extension](https://pub.dev/packages/i18n_extension) translations
package.


* To test the view-model generated by a `VmFactory`, you can now use the static
method `Vm.createFrom(store, factory)`. The method will return the view-model, which you can use
to inspect the view-model properties directly, or call any of the view-model callbacks. Example:

```dart
var store = Store(initialState: User("Mary"));
var vm = Vm.createFrom(store, MyFactory());
// Checking a view-model property.
expect(vm.user.name, "Mary");
// Calling a view-model callback and waiting for the action to finish.
vm.onChangeNameTo("Bill"); // Dispatches SetNameAction("Bill").
await store.waitActionType(SetNameAction);
expect(store.state.name, "Bill");
// Calling a view-model callback and waiting for the state to change.
vm.onChangeNameTo("Bill"); // Dispatches SetNameAction("Bill").
await store.waitCondition((state) => state.name == "Bill");
expect(store.state.name, "Bill");
```


* While the `StoreTester` is a powerful tool with advanced features that are beneficial for the
most complex testing scenarios, for **almost all tests** it's now recommended to use the `Store`
directly. This approach involves waiting for an action to complete its dispatch process or for
the store state to meet a certain condition. After this, you can verify the current state or
action using the new
methods `store.dispatchAndWait`, `store.waitCondition`, `store.waitActionType`,
and `store.waitAnyActionType`. For example:

```dart
// Wait for some action to dispatch and check the state.
await store.dispatchAndWait(MyAction());
expect(store.state.name, 'John')
// Wait for some action to dispatch, and check for errors in the action status.
var status = await dispatchAndWait(MyAction());
expect(status.originalError, isA<UserException>());
// Wait for some state condition
expect(store.state.name, 'John')
dispatch(ChangeNameAction("Bill"));
var action = await store.waitCondition((state) => state.name == "Bill");
expect(action, isA<ChangeNameAction>());
expect(store.state.name, 'Bill');
// Wait until some action of a given type is dispatched.
dispatch(DoALotOfStuffAction());
var action = store.waitActionType(ChangeNameAction);
expect(action, isA<ChangeNameAction>());
expect(action.status.isCompleteOk, isTrue);
expect(store.state.name, 'Bill');
// Wait until some action of any of the given types is dispatched.
dispatch(BuyAction('IBM'));
var action = store.waitAnyActionType([BuyAction, SellAction]);
expect(store.state.portfolio.contains('IBM'), isTrue);
```

# 21.7.1

* DEPRECATION WARNING:
Expand Down Expand Up @@ -169,11 +231,6 @@ showcasing the fundamentals and best practices described in the AsyncRedux READM
extension and put it in the same directory as your `app_state.dart` file containing
your `AppState` class.


* You can now use `var vm = MyFactory().fromStoreTester(storeTester)`
to test a view-model. Read the detailed explanation in the README.md file,
under the title `Testing the StoreConnector's View-model`.

# 21.1.1

* `await StoreTester.dispatchAndWait(action)` dispatches an action, and then waits until it
Expand Down
44 changes: 27 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1504,30 +1504,40 @@ expect(storeTester.lastInfo.state.name, "Mark");

### Testing the StoreConnector's View-model

To test the **view-model** used by a `StoreConnector`, first create a `StoreTester`,
then create the `Factory`, and then call the `factory.fromStoreTester()` method,
passing it the store-tester.
To test the view-model generated by a `VmFactory`, use `createFrom` and pass it the
`store` and the `factory`. Note this method must be called in a recently
created factory, as it can only be called once per factory instance.

You will get the view-model, which you can use to:
The method will return the **view-model**, which you can use to:

* Inspect the view-model properties directly, or

* Call any of the view-model callbacks. If the callbacks dispatch actions,
you can wait for them using the store-tester.
you use `await store.waitActionType(MyAction)`,
or `await store.waitAllActionTypes([MyAction, OtherAction])`,
or `await store.waitCondition((state) => ...)`,
or if necessary you can even record all dispatched actions and state changes
with `Store.record.start()` and `Store.record.stop()`.

Example:

```dart
var storeTester = StoreTester(initialState: User("Mary"));
var vm = MyFactory().fromStoreTester(storeTester);
```
var store = Store(initialState: User("Mary"));
var vm = Vm.createFrom(store, MyFactory());
// Checking a view-model property.
expect(vm.user.name, "Mary");
vm.onChangeNameTo("Bill"); // Suppose it dispatches action SetNameAction("Bill").
var info = await storeTester.wait(SetNameAction);
expect(info.state.name, "Bill");
```
// Calling a view-model callback and waiting for the action to finish.
vm.onChangeNameTo("Bill"); // Dispatches SetNameAction("Bill").
await store.waitActionType(SetNameAction);
expect(store.state.name, "Bill");
Note: _The `fromStoreTester()` method must be called in a recently created factory, as it can be
called only once per factory instance._
// Calling a view-model callback and waiting for the state to change.
vm.onChangeNameTo("Bill"); // Dispatches SetNameAction("Bill").
await store.waitCondition((state) => state.name == "Bill");
expect(store.state.name, "Bill");
```

#### Testing the StoreConnector's onInit and onInit

Expand Down Expand Up @@ -1789,9 +1799,9 @@ Since each widget will have a bunch of related files, you should have some consi
convention. For example, if some dumb-widget is called `MyWidget`, its file could
be `my_widget.dart`. Then the corresponding connector-widget could be `MyWidgetConnector`
in `my_widget_CONNECTOR.dart`. The three corresponding test files could be
named `my_widget_STATE_test.dart`,
`my_widget_CONNECTOR_test.dart` and `my_widget_PRESENTATION_test.dart`. If you don't like this
convention use your own, but just choose one early and stick to it.
named `my_widget_STATE_test.dart`, `my_widget_CONNECTOR_test.dart`
and `my_widget_PRESENTATION_test.dart`. If you don't like this convention use your own,
but just choose one early and stick to it.

<br>

Expand Down
95 changes: 90 additions & 5 deletions lib/src/store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -316,18 +316,103 @@ class Store<St> {
}

/// Returns a future which will complete when the given [condition] is true.
/// The condition can access the state. You may also provide a
/// [timeoutInSeconds], which by default is null (never times out).
Future<void> waitCondition(
/// The condition can access the state. You may also provide a [timeoutInSeconds], which
/// by default is 10 minutes. If you want, you can modify [StoreTester.defaultTimeout] to change
/// the default timeout. Note: To disable the timeout, modify this to a large value,
/// like 300000000 (almost 10 years).
///
/// This method is useful in tests, and it returns the action which changed
/// the store state into the condition, in case you need it:
///
/// ```dart
/// var action = await store.waitCondition((state) => state.name == "Bill");
/// expect(action, isA<ChangeNameAction>());
/// ```
///
/// This method is also eventually useful in production code, in which case you
/// should avoid waiting for conditions that may take a very long time to complete,
/// as checking the condition is an overhead to every state change.
///
Future<ReduxAction<St>> waitCondition(
bool Function(St) condition, {
int? timeoutInSeconds,
}) async {
var conditionTester = StoreTester.simple(this);
try {
await conditionTester.waitCondition(
var info = await conditionTester.waitConditionGetLast(
(TestInfo<St>? info) => condition(info!.state),
timeoutInSeconds: timeoutInSeconds,
timeoutInSeconds: timeoutInSeconds ?? StoreTester.defaultTimeout,
);
var action = info.action;
if (action == null) throw StoreExceptionTimeout();
return action;
} finally {
await conditionTester.cancel();
}
}

/// Returns a future which will complete when an action of the given type is dispatched, and
/// then waits until it finishes. Ignores other actions types.
///
/// You may also provide a [timeoutInSeconds], which by default is 10 minutes.
/// If you want, you can modify [StoreTester.defaultTimeout] to change the default timeout.
///
/// This method returns the action, which you can use to check its `status`:
///
/// ```dart
/// var action = await store.waitActionType(MyAction);
/// expect(action.status.originalError, isA<UserException>());
/// ```
///
/// You should only use this method in tests.
@visibleForTesting
Future<ReduxAction<St>> waitActionType(
Type actionType, {
int? timeoutInSeconds,
}) async {
var conditionTester = StoreTester.simple(this);
try {
var info = await conditionTester.waitUntil(
actionType,
timeoutInSeconds: timeoutInSeconds ?? StoreTester.defaultTimeout,
);
var action = info.action;
if (action == null) throw StoreExceptionTimeout();
return action;
} finally {
await conditionTester.cancel();
}
}

/// Returns a future which will complete when an action of ANY of the given types is dispatched,
/// and then waits until it finishes. Ignores other actions types.
///
/// You may also provide a [timeoutInSeconds], which by default is 10 minutes.
/// If you want, you can modify [StoreTester.defaultTimeout] to change the default timeout.
///
/// This method returns the action, which you can use to check its `status`:
///
/// ```dart
/// var action = await store.waitAnyActionType([MyAction, OtherAction]);
/// expect(action.status.originalError, isA<UserException>());
/// ```
///
/// You should only use this method in tests.
@visibleForTesting
Future<ReduxAction<St>> waitAnyActionType(
List<Type> actionTypes, {
bool ignoreIni = true,
int? timeoutInSeconds,
}) async {
var conditionTester = StoreTester.simple(this);
try {
var info = await conditionTester.waitUntilAny(
actionTypes,
timeoutInSeconds: timeoutInSeconds ?? StoreTester.defaultTimeout,
);
var action = info.action;
if (action == null) throw StoreExceptionTimeout();
return action;
} finally {
await conditionTester.cancel();
}
Expand Down
Loading

0 comments on commit 897d656

Please sign in to comment.