Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Copy expect and async matchers from test package #210

Merged
merged 5 commits into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.12.15-dev

* Add `package:matcher/expect.dart` library. Copies the implementation of
jakemac53 marked this conversation as resolved.
Show resolved Hide resolved
`expect` and the asynchronous matchers from `package:test`.

## 0.12.14

* Add `containsOnce` matcher.
Expand Down
227 changes: 224 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,233 @@ Support for specifying test expectations, such as for unit tests.
The matcher library provides a third-generation assertion mechanism, drawing
inspiration from [Hamcrest](https://code.google.com/p/hamcrest/).

For more information, see
For more information on testing, see
[Unit Testing with Dart](https://github.com/dart-lang/test/blob/master/pkgs/test/README.md#writing-tests).

# Best Practices
## Using matcher

## Prefer semantically meaningful matchers to comparing derived values
Expectations start with a call to [`expect()`] or [`expectAsync()`].

[`expect()`]: https://pub.dev/documentation/matcher/latest/expect/expect.html
[`expectAsync()`]: https://pub.dev/documentation/matcher/latest/expect/expectAsync.html

Any matchers package can be used with `expect()` to do
complex validations:

[`matcher`]: https://pub.dev/documentation/matcher/latest/matcher/matcher-library.html

```dart
import 'package:test/test.dart';

void main() {
test('.split() splits the string on the delimiter', () {
expect('foo,bar,baz', allOf([
contains('foo'),
isNot(startsWith('bar')),
endsWith('baz')
]));
});
}
```

If a non-matcher value is passed, it will be wrapped with [`equals()`].

[`equals()`]: https://pub.dev/documentation/matcher/latest/expect/equals.html

## Exception matchers

You can also test exceptions with the [`throwsA()`] function or a matcher such
as [`throwsFormatException`]:

[`throwsA()`]: https://pub.dev/documentation/matcher/latest/expect/throwsA.html
[`throwsFormatException`]: https://pub.dev/documentation/matcher/latest/expect/throwsFormatException-constant.html

```dart
import 'package:test/test.dart';

void main() {
test('.parse() fails on invalid input', () {
expect(() => int.parse('X'), throwsFormatException);
});
}
```

### Future Matchers

There are a number of useful functions and matchers for more advanced
asynchrony. The [`completion()`] matcher can be used to test `Futures`; it
ensures that the test doesn't finish until the `Future` completes, and runs a
matcher against that `Future`'s value.

[`completion()`]: https://pub.dev/documentation/matcher/latest/expect/completion.html

```dart
import 'dart:async';

import 'package:test/test.dart';

void main() {
test('Future.value() returns the value', () {
expect(Future.value(10), completion(equals(10)));
});
}
```

The [`throwsA()`] matcher and the various [`throwsExceptionType`] matchers work
with both synchronous callbacks and asynchronous `Future`s. They ensure that a
particular type of exception is thrown:

[`throwsExceptionType`]: https://pub.dev/documentation/matcher/latest/expect/throwsException-constant.html

```dart
import 'dart:async';

import 'package:test/test.dart';

void main() {
test('Future.error() throws the error', () {
expect(Future.error('oh no'), throwsA(equals('oh no')));
expect(Future.error(StateError('bad state')), throwsStateError);
});
}
```

The [`expectAsync()`] function wraps another function and has two jobs. First,
it asserts that the wrapped function is called a certain number of times, and
will cause the test to fail if it's called too often; second, it keeps the test
from finishing until the function is called the requisite number of times.

```dart
import 'dart:async';

import 'package:test/test.dart';

void main() {
test('Stream.fromIterable() emits the values in the iterable', () {
var stream = Stream.fromIterable([1, 2, 3]);

stream.listen(expectAsync1((number) {
expect(number, inInclusiveRange(1, 3));
}, count: 3));
});
}
```

[`expectAsync()`]: https://pub.dev/documentation/matcher/latest/expect/expectAsync.html

### Stream Matchers

The `test` package provides a suite of powerful matchers for dealing with
[asynchronous streams][Stream]. They're expressive and composable, and make it
easy to write complex expectations about the values emitted by a stream. For
example:

[Stream]: https://api.dart.dev/stable/dart-async/Stream-class.html

```dart
import 'dart:async';

import 'package:test/test.dart';

void main() {
test('process emits status messages', () {
// Dummy data to mimic something that might be emitted by a process.
var stdoutLines = Stream.fromIterable([
'Ready.',
'Loading took 150ms.',
'Succeeded!'
]);

expect(stdoutLines, emitsInOrder([
// Values match individual events.
'Ready.',

// Matchers also run against individual events.
startsWith('Loading took'),

// Stream matchers can be nested. This asserts that one of two events are
// emitted after the "Loading took" line.
emitsAnyOf(['Succeeded!', 'Failed!']),

// By default, more events are allowed after the matcher finishes
// matching. This asserts instead that the stream emits a done event and
// nothing else.
emitsDone
]));
});
}
```

A stream matcher can also match the [`async`] package's [`StreamQueue`] class,
which allows events to be requested from a stream rather than pushed to the
consumer. The matcher will consume the matched events, but leave the rest of the
queue alone so that it can still be used by the test, unlike a normal `Stream`
which can only have one subscriber. For example:

[`async`]: https://pub.dev/packages/async
[`StreamQueue`]: https://pub.dev/documentation/async/latest/async/StreamQueue-class.html

```dart
import 'dart:async';

import 'package:async/async.dart';
import 'package:test/test.dart';

void main() {
test('process emits a WebSocket URL', () async {
// Wrap the Stream in a StreamQueue so that we can request events.
var stdout = StreamQueue(Stream.fromIterable([
'WebSocket URL:',
'ws://localhost:1234/',
'Waiting for connection...'
]));

// Ignore lines from the process until it's about to emit the URL.
await expectLater(stdout, emitsThrough('WebSocket URL:'));

// Parse the next line as a URL.
var url = Uri.parse(await stdout.next);
expect(url.host, equals('localhost'));

// You can match against the same StreamQueue multiple times.
await expectLater(stdout, emits('Waiting for connection...'));
});
}
```

The following built-in stream matchers are available:

* [`emits()`] matches a single data event.
* [`emitsError()`] matches a single error event.
* [`emitsDone`] matches a single done event.
* [`mayEmit()`] consumes events if they match an inner matcher, without
requiring them to match.
* [`mayEmitMultiple()`] works like `mayEmit()`, but it matches events against
the matcher as many times as possible.
* [`emitsAnyOf()`] consumes events matching one (or more) of several possible
matchers.
* [`emitsInOrder()`] consumes events matching multiple matchers in a row.
* [`emitsInAnyOrder()`] works like `emitsInOrder()`, but it allows the
matchers to match in any order.
* [`neverEmits()`] matches a stream that finishes *without* matching an inner
matcher.

You can also define your own custom stream matchers with [`StreamMatcher()`].

[`emits()`]: https://pub.dev/documentation/matcher/latest/expect/emits.html
[`emitsError()`]: https://pub.dev/documentation/matcher/latest/expect/emitsError.html
[`emitsDone`]: https://pub.dev/documentation/matcher/latest/expect/emitsDone.html
[`mayEmit()`]: https://pub.dev/documentation/matcher/latest/expect/mayEmit.html
[`mayEmitMultiple()`]: https://pub.dev/documentation/matcher/latest/expect/mayEmitMultiple.html
[`emitsAnyOf()`]: https://pub.dev/documentation/matcher/latest/expect/emitsAnyOf.html
[`emitsInOrder()`]: https://pub.dev/documentation/matcher/latest/expect/emitsInOrder.html
[`emitsInAnyOrder()`]: https://pub.dev/documentation/matcher/latest/expect/emitsInAnyOrder.html
[`neverEmits()`]: https://pub.dev/documentation/matcher/latest/expect/neverEmits.html
[`StreamMatcher()`]: https://pub.dev/documentation/matcher/latest/expect/StreamMatcher-class.html

## Best Practices

### Prefer semantically meaningful matchers to comparing derived values

Matchers which have knowledge of the semantics that are tested are able to emit
more meaningful messages which don't require reading test source to understand
Expand Down
64 changes: 64 additions & 0 deletions lib/expect.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// ignore_for_file: deprecated_member_use_from_same_package

export 'matcher.dart';

export 'src/expect/expect.dart' show expect, expectLater, fail;
export 'src/expect/expect_async.dart'
show
Func0,
Func1,
Func2,
Func3,
Func4,
Func5,
Func6,
expectAsync,
expectAsync0,
expectAsync1,
expectAsync2,
expectAsync3,
expectAsync4,
expectAsync5,
expectAsync6,
expectAsyncUntil0,
expectAsyncUntil1,
expectAsyncUntil2,
expectAsyncUntil3,
expectAsyncUntil4,
expectAsyncUntil5,
expectAsyncUntil6;
export 'src/expect/future_matchers.dart'
show completes, completion, doesNotComplete;
export 'src/expect/never_called.dart' show neverCalled;
export 'src/expect/prints_matcher.dart' show prints;
export 'src/expect/stream_matcher.dart' show StreamMatcher;
export 'src/expect/stream_matchers.dart'
show
emitsDone,
emits,
emitsError,
mayEmit,
emitsAnyOf,
emitsInOrder,
emitsInAnyOrder,
emitsThrough,
mayEmitMultiple,
neverEmits;
export 'src/expect/throws_matcher.dart' show throwsA;
export 'src/expect/throws_matchers.dart'
show
throwsArgumentError,
throwsConcurrentModificationError,
throwsCyclicInitializationError,
throwsException,
throwsFormatException,
throwsNoSuchMethodError,
throwsNullThrownError,
throwsRangeError,
throwsStateError,
throwsUnimplementedError,
throwsUnsupportedError;
68 changes: 68 additions & 0 deletions lib/src/expect/async_matcher.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// ignore_for_file: deprecated_member_use_from_same_package

import 'package:test_api/hooks.dart';

import '../description.dart';
import '../equals_matcher.dart';
import '../interfaces.dart';
import '../operator_matchers.dart';
import '../type_matcher.dart';
import 'expect.dart';

/// A matcher that does asynchronous computation.
///
/// Rather than implementing [matches], subclasses implement [matchAsync].
/// [AsyncMatcher.matches] ensures that the test doesn't complete until the
/// returned future completes, and [expect] returns a future that completes when
/// the returned future completes so that tests can wait for it.
abstract class AsyncMatcher extends Matcher {
const AsyncMatcher();

/// Returns `null` if this matches [item], or a [String] description of the
/// failure if it doesn't match.
///
/// This can return a [Future] or a synchronous value. If it returns a
/// [Future], neither [expect] nor the test will complete until that [Future]
/// completes.
///
/// If this returns a [String] synchronously, [expect] will synchronously
/// throw a [TestFailure] and [matches] will synchronously return `false`.
dynamic /*FutureOr<String>*/ matchAsync(dynamic item);

@override
bool matches(dynamic item, Map matchState) {
final result = matchAsync(item);
expect(
result,
anyOf([
equals(null),
const TypeMatcher<Future>(),
const TypeMatcher<String>()
]),
reason: 'matchAsync() may only return a String, a Future, or null.');

if (result is Future) {
final outstandingWork = TestHandle.current.markPending();
result.then((realResult) {
if (realResult != null) {
fail(formatFailure(this, item, realResult as String));
}
outstandingWork.complete();
});
} else if (result is String) {
matchState[this] = result;
return false;
}

return true;
}

@override
Description describeMismatch(dynamic item, Description mismatchDescription,
Map matchState, bool verbose) =>
StringDescription(matchState[this] as String);
}
Loading