Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
28eeb2a
Take Condition for async nesting expectations
natebosch Feb 2, 2023
fe40a54
Rename asyncDescription
natebosch Feb 2, 2023
9f25f8f
Merge branch 'master' into async-take-condition-drop-which
natebosch Feb 2, 2023
8ab184b
Make `label` arguments take callbacks
natebosch Feb 3, 2023
7a0724f
Expand the doc comment for Context
natebosch Feb 3, 2023
10e6f47
Compare the non-nesting label case to a clause directly
natebosch Feb 3, 2023
bac2b3e
Mention that which can be omitted
natebosch Feb 3, 2023
8529702
dart format
natebosch Feb 3, 2023
4e4cf10
Enforce single string still in root ctor
natebosch Feb 4, 2023
00888e6
Rename _StringClause to _ExpectationClause
natebosch Feb 4, 2023
308b13d
Update docs for nest and nestAsync
natebosch Feb 4, 2023
e35574b
Consistently use double quotes to avoid escape single quote
natebosch Feb 4, 2023
915652d
Merge commit 'e35574bb' into label-callback--context-docs
natebosch Feb 4, 2023
511fa70
Merge branch 'master' into label-callback--context-docs
natebosch Feb 4, 2023
911659b
Attempt to present information in a better order
natebosch Feb 4, 2023
547abd3
More wording tweaks, add doc header on ConditionSubject
natebosch Feb 4, 2023
ef6bd7e
More wording tweaks and a paragraph on softCheck subjects
natebosch Feb 4, 2023
99f9e2f
Move the 'should not throw' phrase into a template that is used in th…
natebosch Feb 4, 2023
9400186
Merge branch 'master' into async-take-condition-drop-which
natebosch Feb 4, 2023
5a09c33
Merge branch 'master' into async-take-condition-drop-which
natebosch Feb 4, 2023
2ddbd46
Tweaks
natebosch Feb 4, 2023
d215a20
Merge branch 'label-callback--context-docs' into async-take-condition…
natebosch Feb 4, 2023
7c9fce9
Add missing generic, use {} over =>
natebosch Feb 4, 2023
3ca1aaf
Make the fields of Extracted private
natebosch Feb 4, 2023
db4fc6f
More working tweaks, export ConditionSubject
natebosch Feb 4, 2023
7890a64
Merge branch 'label-callback--context-docs' into async-take-condition…
natebosch Feb 4, 2023
b33a327
Merge branch 'master' into label-callback--context-docs
natebosch Feb 4, 2023
9d7fb07
Merge branch 'label-callback--context-docs' into async-take-condition…
natebosch Feb 4, 2023
6b38bff
Typo
natebosch Feb 4, 2023
330c3f6
Expectation extension method in Subject doc
natebosch Feb 4, 2023
4d14182
missing word
natebosch Feb 6, 2023
6a8e670
Link to example extensions, expand callback phrasing
natebosch Feb 6, 2023
afc09b3
Link the specific methods earlier
natebosch Feb 6, 2023
148ef9d
Move section on callbacks being unused to much later
natebosch Feb 6, 2023
bae48cb
Add some examples
natebosch Feb 6, 2023
e09f2ae
Revert mistaken change
natebosch Feb 6, 2023
b9b8960
Merge branch 'master' into label-callback--context-docs
natebosch Feb 6, 2023
f17581f
Merge branch 'label-callback--context-docs' into async-take-condition…
natebosch Feb 6, 2023
bfc06ed
Merge branch 'master' into async-take-condition-drop-which
natebosch Feb 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkgs/checks/lib/checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

export 'src/checks.dart' show checkThat, Subject, Skip, it;
export 'src/extensions/async.dart'
show ChainAsync, FutureChecks, StreamChecks, StreamQueueWrap;
show FutureChecks, StreamChecks, StreamQueueWrap;
export 'src/extensions/core.dart'
show BoolChecks, CoreChecks, NullabilityChecks;
export 'src/extensions/function.dart' show ThrowsCheck;
Expand Down
33 changes: 19 additions & 14 deletions pkgs/checks/lib/src/checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,10 @@ abstract class Context<T> {
/// Some context may disallow asynchronous expectations, for instance in
/// [softCheck] which must synchronously check the value. In those contexts
/// this method will throw.
Future<Subject<R>> nestAsync<R>(
String label, FutureOr<Extracted<R>> Function(T) extract);
Future<void> nestAsync<R>(
String label,
FutureOr<Extracted<R>> Function(T) extract,
Condition<R>? nestedCondition);
}

/// A property extracted from a value being checked, or a rejection.
Expand Down Expand Up @@ -467,8 +469,10 @@ class _TestContext<T> implements Context<T>, _ClauseDescription {
}

@override
Future<Subject<R>> nestAsync<R>(
String label, FutureOr<Extracted<R>> Function(T) extract) async {
Future<void> nestAsync<R>(
String label,
FutureOr<Extracted<R>> Function(T) extract,
Condition<R>? nestedCondition) async {
if (!_allowAsync) {
throw StateError(
'Async expectations cannot be used on a synchronous subject');
Expand All @@ -485,7 +489,7 @@ class _TestContext<T> implements Context<T>, _ClauseDescription {
final value = result.value ?? _Absent<R>();
final context = _TestContext<R>._child(value, label, this);
_clauses.add(context);
return Subject._(context);
await nestedCondition?.applyAsync(Subject<R>._(context));
}

CheckFailure _failure(Rejection rejection) =>
Expand Down Expand Up @@ -557,9 +561,11 @@ class _SkippedContext<T> implements Context<T> {
}

@override
Future<Subject<R>> nestAsync<R>(
String label, FutureOr<Extracted<R>> Function(T p1) extract) async {
return Subject._(_SkippedContext());
Future<void> nestAsync<R>(
String label,
FutureOr<Extracted<R>> Function(T p1) extract,
Condition<R>? nestedCondition) async {
// no-op
}
}

Expand Down Expand Up @@ -778,13 +784,12 @@ class _ReplayContext<T> implements Context<T>, Condition<T> {
}

@override
Future<Subject<R>> nestAsync<R>(
String label, FutureOr<Extracted<R>> Function(T) extract) async {
final nestedContext = _ReplayContext<R>();
Future<void> nestAsync<R>(
String label,
FutureOr<Extracted<R>> Function(T) extract,
Condition<R>? nestedCondition) async {
_interactions.add((c) async {
var result = await c.nestAsync(label, extract);
await nestedContext.applyAsync(result);
await c.nestAsync(label, extract, nestedCondition);
});
return Subject._(nestedContext);
}
}
37 changes: 10 additions & 27 deletions pkgs/checks/lib/src/extensions/async.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extension FutureChecks<T> on Subject<Future<T>> {
/// future completes.
///
/// Fails if the future completes as an error.
Future<Subject<T>> completes() =>
Future<void> completes([Condition<T>? completionCondition]) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the original here. It's explicit.
I assume that if the future has an error, the returned future has a test-failure error. (Which becomes an uncaught error if you don't do anything, which is fine, because we run tests in error zone.)

The alternative here seems to be, effectively doing a then for you, so

  check(myFuture).completes(conditon);

is equivalent to the original

  check(myFuture).completes().then(condition.applyAsync);

I don't think I'd want to use a Condition object directly if I can avoid it.

I see three options here:

Left: Return Future<Subject<T>>, and I assume it's a test failure if the future had an error.
Right: Pass in test

Alternative: Return an "AsynchronousSubject" which accepts all the same tests as a normal Subject, but won't apply them until the future completes. (I think I tried that, and it got very complicated very quickly.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that if the future has an error, the returned future has a test-failure error. (Which becomes an uncaught error if you don't do anything, which is fine, because we run tests in error zone.)

Correct.

Alternative: Return an "AsynchronousSubject" which accepts all the same tests as a normal Subject, but won't apply them until the future completes. (I think I tried that, and it got very complicated very quickly.)

This also wouldn't allow for waiting for the expectation to pass before doing more work or checking more expectations. Any existing test doing await expectLater today would not have a migration pattern if we lost the ability to return a Future for async expectations.

Copy link
Member Author

@natebosch natebosch Feb 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right: Pass in test

The condition is optional.

In both cases .completes() behave identically - cause a failure if the future has en error, otherwise succeed.

With the current code, chaining has multiple choices for how to write it. In order of preference for how I would write it:

await check(actual).completes().which(it()..furtherCondition());
(await check(actual).completes()).furtherCondition();
await check(actual).completes().then((result) => result.furtherCondition());
final result = await check(actual).completes();
result.furtherCondition();

After this PR, this would collapse to one choice

await check(actual).completes(it()..furtherCondition));

I like

  • That there is only 1 option.
  • That the 1 option reads nicer than any of the multiple choice options IMO.

I'm less sure that we can confidently say there are no valid use cases for choosing one of the other patterns. I'm also less sure that everyone will have the distaste for (await check(actual).completes()).furtherCondition() that I picked up while working on this package.

context.nestAsync<T>('completes to a value', (actual) async {
try {
return Extracted.value(await actual);
Expand All @@ -27,7 +27,7 @@ extension FutureChecks<T> on Subject<Future<T>> {
...(const LineSplitter()).convert(st.toString())
]);
}
});
}, completionCondition);

/// Expects that the `Future` never completes as a value or an error.
///
Expand Down Expand Up @@ -60,7 +60,8 @@ extension FutureChecks<T> on Subject<Future<T>> {
/// future completes as an error.
///
/// Fails if the future completes to a value.
Future<Subject<E>> throws<E extends Object>() => context.nestAsync<E>(
Future<void> throws<E extends Object>([Condition<E>? errorCondition]) =>
context.nestAsync<E>(
'completes to an error${E == Object ? '' : ' of type $E'}',
(actual) async {
try {
Expand All @@ -77,7 +78,7 @@ extension FutureChecks<T> on Subject<Future<T>> {
...(const LineSplitter()).convert(st.toString())
]);
}
});
}, errorCondition);
}

/// Expectations on a [StreamQueue].
Expand Down Expand Up @@ -109,7 +110,7 @@ extension StreamChecks<T> on Subject<StreamQueue<T>> {
///
/// Fails if the stream emits an error instead of a value, or closes without
/// emitting a value.
Future<Subject<T>> emits() =>
Future<void> emits([Condition<T>? emittedCondition]) =>
context.nestAsync<T>('emits a value', (actual) async {
if (!await actual.hasNext) {
return Extracted.rejection(
Expand All @@ -127,7 +128,7 @@ extension StreamChecks<T> on Subject<StreamQueue<T>> {
...(const LineSplitter()).convert(st.toString())
]);
}
});
}, emittedCondition);

/// Expects that the stream emits an error of type [E].
///
Expand All @@ -140,8 +141,8 @@ extension StreamChecks<T> on Subject<StreamQueue<T>> {
/// If this expectation fails, the source queue will be left in it's original
/// state.
/// If this expectation succeeds, consumes the error event.
Future<Subject<E>> emitsError<E extends Object>() =>
context.nestAsync('emits an error${E == Object ? '' : ' of type $E'}',
Future<void> emitsError<E extends Object>([Condition<E>? errorCondition]) =>
context.nestAsync<E>('emits an error${E == Object ? '' : ' of type $E'}',
(actual) async {
if (!await actual.hasNext) {
return Extracted.rejection(
Expand All @@ -164,7 +165,7 @@ extension StreamChecks<T> on Subject<StreamQueue<T>> {
...(const LineSplitter()).convert(st.toString())
]);
}
});
}, errorCondition);

/// Expects that the `Stream` emits any number of events before emitting an
/// event that satisfies [condition].
Expand Down Expand Up @@ -436,24 +437,6 @@ extension StreamChecks<T> on Subject<StreamQueue<T>> {
}
}

extension ChainAsync<T> on Future<Subject<T>> {
/// Checks the expectations in [condition] against the result of this
/// `Future`.
///
/// Extensions written on [Subject] cannot be invoked on a `Future<Subject>`.
/// This method allows adding expectations for the value without awaiting an
/// expression that would need parenthesis.
///
/// ```dart
/// await checkThat(someFuture).completes().which(it()..equals('expected'));
/// // or, with the intermediate `await`:
/// (await checkThat(someFuture).completes()).equals('expected');
/// ```
Future<void> which(Condition<T> condition) async {
await condition.applyAsync(await this);
}
}

extension StreamQueueWrap<T> on Subject<Stream<T>> {
/// Wrap the stream in a [StreamQueue] to allow using checks from
/// [StreamChecks].
Expand Down
Loading