Skip to content

Commit ffeaec6

Browse files
authored
Take Condition for async nesting expectations (#1896)
Avoid the awkwardness of awaiting before chaining conditions, or needing a `which` utility to chain on a Future. This introduces a discrepancy between chaining for sync vs async expectations, but it makes many common async expectations nicer to read. - Remove `which` and a reference to it in README. - Update `nestAsync` to take `nestedCondition` argument instead of returning a `Subject`. This forces all async conditions to follow the same pattern. - Update `nestAsync` doc example to use the new signature. - Mention how to check subsequent conditions in the doc for `nest`, and in the doc for `nestAsync` explicitly contrast with `nest`. - Remove now unnecessary nesting handling from `_ReplayContext`. - Update all async expectations using `nestAsync` to use the new signature and describe usage in their docs. - Add `_LazyCondition` in `test_shared.dart`. This could prove useful enough to put directly in `Condition` at some point. This allows the condition that runs to be based on the `actual` value from the predicate, which is an unusual situation for testing within the library since it mirrors the defaulting that happens in `_TestContext`.
1 parent cdd8c39 commit ffeaec6

File tree

7 files changed

+272
-239
lines changed

7 files changed

+272
-239
lines changed

pkgs/checks/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44
- `checkThat` renamed to `check`.
55
- `nest` and `nestAsync` take `Iterable<String> Function()` arguments for
66
`label` instead of `String`.
7+
- Async expectation extensions `completes`, `throws`, `emits`, and
8+
`emitsError` no longer return a `Future<Subject>`. Instead they take an
9+
optional `Condition` argument which can check expectations that would
10+
have been checked on the returned subject.
11+
- `nestAsync` no longer returns a `Subject`, callers must pass the
12+
followup `Condition` to the nullable argument.
13+
- Remove the `which` extension on `Future<Subject>`.
14+
- Add a constructor for `Condition` which takes a callback to invoke when
15+
`apply` or `applyAsync` is called.
716
- Added an example.
817
- Include a stack trace in the failure description for unexpected errors from
918
Futures or Streams.

pkgs/checks/README.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,8 @@ condition. The `it()` utility returns a `ConditionSubject`.
5050
check(someList).any(it()..isGreaterThan(0));
5151
```
5252

53-
Some complicated checks may be difficult to write with parenthesized awaited
54-
expressions, or impossible to write with cascade syntax. There are `which`
55-
utilities for both use cases which take a `Condition`.
53+
Some complicated checks may be not be possible to write with cascade syntax.
54+
There is a `which` utility for this use case which takes a `Condition`.
5655

5756
```dart
5857
check(someString)
@@ -61,10 +60,6 @@ check(someString)
6160
..length.which(it()
6261
..isGreatherThan(10)
6362
..isLessThan(100));
64-
65-
await check(someFuture)
66-
.completes()
67-
.which(it()..equals(expectedCompletion));
6863
```
6964

7065
# Writing custom expectations

pkgs/checks/lib/checks.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
export 'src/checks.dart' show SkipExtension, Subject, check, it;
66
export 'src/extensions/async.dart'
7-
show ChainAsync, FutureChecks, StreamChecks, WithQueueExtension;
7+
show FutureChecks, StreamChecks, WithQueueExtension;
88
export 'src/extensions/core.dart' show BoolChecks, CoreChecks, NullableChecks;
99
export 'src/extensions/function.dart' show FunctionChecks;
1010
export 'src/extensions/iterable.dart' show IterableChecks;

pkgs/checks/lib/src/checks.dart

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,17 @@ Future<Iterable<String>> describeAsync<T>(Condition<T> condition) async {
178178

179179
/// A set of expectations that are checked against the value when applied to a
180180
/// [Subject].
181+
///
182+
/// This class should not be implemented or extended.
181183
abstract class Condition<T> {
184+
/// Check the expectations of this condition against [subject].
185+
///
186+
/// The [subject] should throw if any asynchronous expectations are checked.
187+
/// It is not possible to wait for for asynchronous expectations to be fully
188+
/// applied with this method.
182189
void apply(Subject<T> subject);
190+
191+
/// Check the expectations of this condition against [subject].
183192
Future<void> applyAsync(Subject<T> subject);
184193
}
185194

@@ -481,6 +490,9 @@ abstract class Context<T> {
481490
/// [Extracted.rejection] describing the problem. Otherwise it should return
482491
/// an [Extracted.value].
483492
///
493+
/// Subsequent expectations can be checked for the extracted value on the
494+
/// returned [Subject].
495+
///
484496
/// {@macro label_description}
485497
///
486498
/// If [atSameLevel] is true then the returned [Extracted.value] should hold
@@ -518,6 +530,10 @@ abstract class Context<T> {
518530
/// [Extracted.rejection] describing the problem. Otherwise it should return
519531
/// an [Extracted.value].
520532
///
533+
/// In contrast to [nest], subsequent expectations need to be passed in
534+
/// [nestedCondition] which will be applied to the subject for the extracted
535+
/// value.
536+
///
521537
/// {@macro label_description}
522538
///
523539
/// {@macro description_lines}
@@ -527,17 +543,19 @@ abstract class Context<T> {
527543
/// {@macro async_limitations}
528544
///
529545
/// ```dart
530-
/// Future<Subject<Foo>> get someAsyncValue async => await context
531-
/// .nestAsync(() => ['has someAsyncValue'], (actual) async {
532-
/// if (await _cannotReadAsyncValue(actual)) {
533-
/// return Extracted.rejection(
534-
/// which: ['cannot read someAsyncValue']);
535-
/// }
536-
/// return Extracted.value(await _readAsyncValue(actual));
537-
/// });
546+
/// Future<void> someAsyncResult([Condition<Result> resultCondition]) async {
547+
/// await context.nestAsync(() => ['has someAsyncResult'], (actual) async {
548+
/// if (await _asyncOperationFailed(actual)) {
549+
/// return Extracted.rejection(which: ['cannot read someAsyncResult']);
550+
/// }
551+
/// return Extracted.value(await _readAsyncResult(actual));
552+
/// }, resultCondition);
553+
/// }
538554
/// ```
539-
Future<Subject<R>> nestAsync<R>(Iterable<String> Function() label,
540-
FutureOr<Extracted<R>> Function(T) extract);
555+
Future<void> nestAsync<R>(
556+
Iterable<String> Function() label,
557+
FutureOr<Extracted<R>> Function(T) extract,
558+
Condition<R>? nestedCondition);
541559
}
542560

543561
/// A property extracted from a value being checked, or a rejection.
@@ -752,8 +770,10 @@ class _TestContext<T> implements Context<T>, _ClauseDescription {
752770
}
753771

754772
@override
755-
Future<Subject<R>> nestAsync<R>(Iterable<String> Function() label,
756-
FutureOr<Extracted<R>> Function(T) extract) async {
773+
Future<void> nestAsync<R>(
774+
Iterable<String> Function() label,
775+
FutureOr<Extracted<R>> Function(T) extract,
776+
Condition<R>? nestedCondition) async {
757777
if (!_allowAsync) {
758778
throw StateError(
759779
'Async expectations cannot be used on a synchronous subject');
@@ -770,7 +790,7 @@ class _TestContext<T> implements Context<T>, _ClauseDescription {
770790
final value = result._value ?? _Absent<R>();
771791
final context = _TestContext<R>._child(value, label, this);
772792
_clauses.add(context);
773-
return Subject._(context);
793+
await nestedCondition?.applyAsync(Subject<R>._(context));
774794
}
775795

776796
CheckFailure _failure(Rejection rejection) =>
@@ -843,9 +863,11 @@ class _SkippedContext<T> implements Context<T> {
843863
}
844864

845865
@override
846-
Future<Subject<R>> nestAsync<R>(Iterable<String> Function() label,
847-
FutureOr<Extracted<R>> Function(T p1) extract) async {
848-
return Subject._(_SkippedContext());
866+
Future<void> nestAsync<R>(
867+
Iterable<String> Function() label,
868+
FutureOr<Extracted<R>> Function(T p1) extract,
869+
Condition<R>? nestedCondition) async {
870+
// no-op
849871
}
850872
}
851873

@@ -1066,13 +1088,12 @@ class _ReplayContext<T> implements Context<T>, Condition<T> {
10661088
}
10671089

10681090
@override
1069-
Future<Subject<R>> nestAsync<R>(Iterable<String> Function() label,
1070-
FutureOr<Extracted<R>> Function(T) extract) async {
1071-
final nestedContext = _ReplayContext<R>();
1091+
Future<void> nestAsync<R>(
1092+
Iterable<String> Function() label,
1093+
FutureOr<Extracted<R>> Function(T) extract,
1094+
Condition<R>? nestedCondition) async {
10721095
_interactions.add((c) async {
1073-
var result = await c.nestAsync(label, extract);
1074-
await nestedContext.applyAsync(result);
1096+
await c.nestAsync(label, extract, nestedCondition);
10751097
});
1076-
return Subject._(nestedContext);
10771098
}
10781099
}

0 commit comments

Comments
 (0)