feature: Upgrade TypeMatcher, deprecate isInstanceOf (#88)
`TypeMatcher`
- No longer abstract
- Added type parameter
- Deprecate the existing `name` parameter, tell folks to the type param
- Added `having` method which allows chained validation of features
- Eliminated 13 private implementations from the package
- Just use it directly.
- Moved to its own file
Deprecate `isInstanceOf` class.
- Tell folks to use `TypeMatcher<T>` instead
- Run away from weirdly named classes
Tests
- centralizing tests in type_matcher_test
- Removed isInstanceOf tests from core_matchers_test
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2c7d11e..2f238d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,25 @@
+## 0.12.3
+
+- Many improvements to `TypeMatcher`
+ - Can now be used directly as `const TypeMatcher<MyType>()`.
+ - Added a type parameter to specify the target `Type`.
+ - Made the `name` constructor parameter optional and marked it deprecated.
+ It's redundant to the type parameter.
+ - Migrated all `isType` matchers to `TypeMatcher`.
+ - Added a `having` function that allows chained validations of specific
+ features of the target type.
+
+ ```dart
+ /// Validates that the object is a [RangeError] with a message containing
+ /// the string 'details' and `start` and `end` properties that are `null`.
+ final _rangeMatcher = isRangeError
+ .having((e) => e.message, 'message', contains('details'))
+ .having((e) => e.start, 'start', isNull)
+ .having((e) => e.end, 'end', isNull);
+ ```
+
+- Deprecated the `isInstanceOf` class. Use `TypeMatcher` instead.
+
## 0.12.2+1
- Updated SDK version to 2.0.0-dev.17.0
diff --git a/lib/matcher.dart b/lib/matcher.dart
index d532926..72918aa 100644
--- a/lib/matcher.dart
+++ b/lib/matcher.dart
@@ -15,4 +15,5 @@
export 'src/operator_matchers.dart';
export 'src/order_matchers.dart';
export 'src/string_matchers.dart';
+export 'src/type_matcher.dart';
export 'src/util.dart';
diff --git a/lib/src/core_matchers.dart b/lib/src/core_matchers.dart
index 681f206..1de4b8d 100644
--- a/lib/src/core_matchers.dart
+++ b/lib/src/core_matchers.dart
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
import 'interfaces.dart';
+import 'type_matcher.dart';
import 'util.dart';
/// Returns a matcher that matches the isEmpty property.
@@ -103,24 +104,14 @@
Description describe(Description description) => description.add('anything');
}
+/// **DEPRECATED** Use [TypeMatcher] instead.
+///
/// Returns a matcher that matches if an object is an instance
/// of [T] (or a subtype).
-///
-/// As types are not first class objects in Dart we can only
-/// approximate this test by using a generic wrapper class.
-///
-/// For example, to test whether 'bar' is an instance of type
-/// 'Foo', we would write:
-///
-/// expect(bar, new isInstanceOf<Foo>());
+@Deprecated('Use `const TypeMatcher<MyType>()` instead.')
// ignore: camel_case_types
-class isInstanceOf<T> extends Matcher {
+class isInstanceOf<T> extends TypeMatcher<T> {
const isInstanceOf();
-
- bool matches(item, Map matchState) => item is T;
-
- Description describe(Description description) =>
- description.add('an instance of $T');
}
/// A matcher that matches a function call against no exception.
@@ -157,48 +148,11 @@
}
}
-/*
- * Matchers for different exception types. Ideally we should just be able to
- * use something like:
- *
- * final Matcher throwsException =
- * const _Throws(const isInstanceOf<Exception>());
- *
- * Unfortunately instanceOf is not working with dart2js.
- *
- * Alternatively, if static functions could be used in const expressions,
- * we could use:
- *
- * bool _isException(x) => x is Exception;
- * final Matcher isException = const _Predicate(_isException, "Exception");
- * final Matcher throwsException = const _Throws(isException);
- *
- * But currently using static functions in const expressions is not supported.
- * For now the only solution for all platforms seems to be separate classes
- * for each exception type.
- */
+/// A matcher for [Map].
+const isMap = const TypeMatcher<Map>();
-abstract class TypeMatcher extends Matcher {
- final String _name;
- const TypeMatcher(this._name);
- Description describe(Description description) => description.add(_name);
-}
-
-/// A matcher for Map types.
-const Matcher isMap = const _IsMap();
-
-class _IsMap extends TypeMatcher {
- const _IsMap() : super("Map");
- bool matches(item, Map matchState) => item is Map;
-}
-
-/// A matcher for List types.
-const Matcher isList = const _IsList();
-
-class _IsList extends TypeMatcher {
- const _IsList() : super('List');
- bool matches(item, Map matchState) => item is List;
-}
+/// A matcher for [List].
+const isList = const TypeMatcher<List>();
/// Returns a matcher that matches if an object has a length property
/// that matches [matcher].
diff --git a/lib/src/error_matchers.dart b/lib/src/error_matchers.dart
index 1f37538..eb185f4 100644
--- a/lib/src/error_matchers.dart
+++ b/lib/src/error_matchers.dart
@@ -2,94 +2,39 @@
// 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.
-import 'core_matchers.dart';
-import 'interfaces.dart';
+import 'type_matcher.dart';
-/// A matcher for ArgumentErrors.
-const Matcher isArgumentError = const _ArgumentError();
+/// A matcher for [ArgumentError].
+const isArgumentError = const TypeMatcher<ArgumentError>();
-class _ArgumentError extends TypeMatcher {
- const _ArgumentError() : super("ArgumentError");
- bool matches(item, Map matchState) => item is ArgumentError;
-}
+/// A matcher for [ConcurrentModificationError].
+const isConcurrentModificationError =
+ const TypeMatcher<ConcurrentModificationError>();
-/// A matcher for ConcurrentModificationError.
-const Matcher isConcurrentModificationError =
- const _ConcurrentModificationError();
+/// A matcher for [CyclicInitializationError].
+const isCyclicInitializationError =
+ const TypeMatcher<CyclicInitializationError>();
-class _ConcurrentModificationError extends TypeMatcher {
- const _ConcurrentModificationError() : super("ConcurrentModificationError");
- bool matches(item, Map matchState) => item is ConcurrentModificationError;
-}
+/// A matcher for [Exception].
+const isException = const TypeMatcher<Exception>();
-/// A matcher for CyclicInitializationError.
-const Matcher isCyclicInitializationError = const _CyclicInitializationError();
+/// A matcher for [FormatException].
+const isFormatException = const TypeMatcher<FormatException>();
-class _CyclicInitializationError extends TypeMatcher {
- const _CyclicInitializationError() : super("CyclicInitializationError");
- bool matches(item, Map matchState) => item is CyclicInitializationError;
-}
+/// A matcher for [NoSuchMethodError].
+const isNoSuchMethodError = const TypeMatcher<NoSuchMethodError>();
-/// A matcher for Exceptions.
-const Matcher isException = const _Exception();
+/// A matcher for [NullThrownError].
+const isNullThrownError = const TypeMatcher<NullThrownError>();
-class _Exception extends TypeMatcher {
- const _Exception() : super("Exception");
- bool matches(item, Map matchState) => item is Exception;
-}
+/// A matcher for [RangeError].
+const isRangeError = const TypeMatcher<RangeError>();
-/// A matcher for FormatExceptions.
-const Matcher isFormatException = const _FormatException();
+/// A matcher for [StateError].
+const isStateError = const TypeMatcher<StateError>();
-class _FormatException extends TypeMatcher {
- const _FormatException() : super("FormatException");
- bool matches(item, Map matchState) => item is FormatException;
-}
+/// A matcher for [UnimplementedError].
+const isUnimplementedError = const TypeMatcher<UnimplementedError>();
-/// A matcher for NoSuchMethodErrors.
-const Matcher isNoSuchMethodError = const _NoSuchMethodError();
-
-class _NoSuchMethodError extends TypeMatcher {
- const _NoSuchMethodError() : super("NoSuchMethodError");
- bool matches(item, Map matchState) => item is NoSuchMethodError;
-}
-
-/// A matcher for NullThrownError.
-const Matcher isNullThrownError = const _NullThrownError();
-
-class _NullThrownError extends TypeMatcher {
- const _NullThrownError() : super("NullThrownError");
- bool matches(item, Map matchState) => item is NullThrownError;
-}
-
-/// A matcher for RangeErrors.
-const Matcher isRangeError = const _RangeError();
-
-class _RangeError extends TypeMatcher {
- const _RangeError() : super("RangeError");
- bool matches(item, Map matchState) => item is RangeError;
-}
-
-/// A matcher for StateErrors.
-const Matcher isStateError = const _StateError();
-
-class _StateError extends TypeMatcher {
- const _StateError() : super("StateError");
- bool matches(item, Map matchState) => item is StateError;
-}
-
-/// A matcher for UnimplementedErrors.
-const Matcher isUnimplementedError = const _UnimplementedError();
-
-class _UnimplementedError extends TypeMatcher {
- const _UnimplementedError() : super("UnimplementedError");
- bool matches(item, Map matchState) => item is UnimplementedError;
-}
-
-/// A matcher for UnsupportedError.
-const Matcher isUnsupportedError = const _UnsupportedError();
-
-class _UnsupportedError extends TypeMatcher {
- const _UnsupportedError() : super("UnsupportedError");
- bool matches(item, Map matchState) => item is UnsupportedError;
-}
+/// A matcher for [UnsupportedError].
+const isUnsupportedError = const TypeMatcher<UnsupportedError>();
diff --git a/lib/src/having_matcher.dart b/lib/src/having_matcher.dart
new file mode 100644
index 0000000..1684a93
--- /dev/null
+++ b/lib/src/having_matcher.dart
@@ -0,0 +1,62 @@
+// Copyright (c) 2018, 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.
+
+import 'custom_matcher.dart';
+import 'interfaces.dart';
+import 'type_matcher.dart';
+import 'util.dart';
+
+/// A package-private [TypeMatcher] implementation that handles is returned
+/// by calls to [TypeMatcher.having].
+class HavingMatcher<T> implements TypeMatcher<T> {
+ final TypeMatcher<T> _parent;
+ final List<_FunctionMatcher> _functionMatchers;
+
+ HavingMatcher(TypeMatcher<T> parent, String description,
+ Object feature(T source), Object matcher,
+ [Iterable<_FunctionMatcher> existing])
+ : this._parent = parent,
+ this._functionMatchers = <_FunctionMatcher>[]
+ ..addAll(existing ?? [])
+ ..add(new _FunctionMatcher<T>(description, feature, matcher));
+
+ TypeMatcher<T> having(
+ Object feature(T source), String description, Object matcher) =>
+ new HavingMatcher(
+ _parent, description, feature, matcher, _functionMatchers);
+
+ bool matches(item, Map matchState) {
+ for (var matcher in <Matcher>[_parent].followedBy(_functionMatchers)) {
+ if (!matcher.matches(item, matchState)) {
+ addStateInfo(matchState, {'matcher': matcher});
+ return false;
+ }
+ }
+ return true;
+ }
+
+ Description describeMismatch(
+ item, Description mismatchDescription, Map matchState, bool verbose) {
+ var matcher = matchState['matcher'] as Matcher;
+ matcher.describeMismatch(
+ item, mismatchDescription, matchState['state'] as Map, verbose);
+ return mismatchDescription;
+ }
+
+ Description describe(Description description) => description
+ .add('')
+ .addDescriptionOf(_parent)
+ .add(' with ')
+ .addAll('', ' and ', '', _functionMatchers);
+}
+
+class _FunctionMatcher<T> extends CustomMatcher {
+ final dynamic Function(T value) _feature;
+
+ _FunctionMatcher(String name, this._feature, matcher)
+ : super('`$name`:', '`$name`', matcher);
+
+ @override
+ Object featureValueOf(covariant T actual) => _feature(actual);
+}
diff --git a/lib/src/type_matcher.dart b/lib/src/type_matcher.dart
new file mode 100644
index 0000000..91552c9
--- /dev/null
+++ b/lib/src/type_matcher.dart
@@ -0,0 +1,89 @@
+// Copyright (c) 2018, 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.
+
+import 'having_matcher.dart';
+import 'interfaces.dart';
+
+/// A [Matcher] subclass that supports validating the [Type] of the target
+/// object.
+///
+/// ```dart
+/// expect(shouldBeDuration, new TypeMatcher<Duration>());
+/// ```
+///
+/// If you want to further validate attributes of the specified [Type], use the
+/// [having] function.
+///
+/// ```dart
+/// void shouldThrowRangeError(int value) {
+/// throw new RangeError.range(value, 10, 20);
+/// }
+///
+/// expect(
+/// () => shouldThrowRangeError(5),
+/// throwsA(const TypeMatcher<RangeError>()
+/// .having((e) => e.start, 'start', greaterThanOrEqualTo(10))
+/// .having((e) => e.end, 'end', lessThanOrEqualTo(20))));
+/// ```
+///
+/// Notice that you can chain multiple calls to [having] to verify multiple
+/// aspects of an object.
+///
+/// Note: All of the top-level `isType` matchers exposed by this package are
+/// instances of [TypeMatcher], so you can use the [having] function without
+/// creating your own instance.
+///
+/// ```dart
+/// expect(
+/// () => shouldThrowRangeError(5),
+/// throwsA(isRangeError
+/// .having((e) => e.start, 'start', greaterThanOrEqualTo(10))
+/// .having((e) => e.end, 'end', lessThanOrEqualTo(20))));
+/// ```
+class TypeMatcher<T> extends Matcher {
+ final String _name;
+ const TypeMatcher(
+ [@Deprecated('Provide a type argument to TypeMatcher and omit the name. '
+ 'This argument will be removed in the next release.')
+ String name])
+ : this._name =
+ // ignore: deprecated_member_use
+ name;
+
+ /// Returns a new [TypeMatcher] that validates the existing type as well as
+ /// a specific [feature] of the object with the provided [matcher].
+ ///
+ /// Provides a human-readable [description] of the [feature] to make debugging
+ /// failures easier.
+ ///
+ /// ```dart
+ /// /// Validates that the object is a [RangeError] with a message containing
+ /// /// the string 'details' and `start` and `end` properties that are `null`.
+ /// final _rangeMatcher = isRangeError
+ /// .having((e) => e.message, 'message', contains('details'))
+ /// .having((e) => e.start, 'start', isNull)
+ /// .having((e) => e.end, 'end', isNull);
+ /// ```
+ TypeMatcher<T> having(
+ Object feature(T source), String description, Object matcher) =>
+ new HavingMatcher(this, description, feature, matcher);
+
+ Description describe(Description description) {
+ var name = _name ?? _stripDynamic(T);
+ return description.add("<Instance of '$name'>");
+ }
+
+ bool matches(Object item, Map matchState) => item is T;
+}
+
+final _dart2DynamicArgs = new RegExp('<dynamic(, dynamic)*>');
+
+/// With this expression `{}.runtimeType.toString`,
+/// Dart 1: "<Instance of Map>
+/// Dart 2: "<Instance of Map<dynamic, dynamic>>"
+///
+/// This functions returns the Dart 1 output, when Dart 2 runtime semantics
+/// are enabled.
+String _stripDynamic(Type type) =>
+ type.toString().replaceAll(_dart2DynamicArgs, '');
diff --git a/test/core_matchers_test.dart b/test/core_matchers_test.dart
index b5e75b1..48e7672 100644
--- a/test/core_matchers_test.dart
+++ b/test/core_matchers_test.dart
@@ -228,12 +228,6 @@
shouldFail(actual3, equals(expected3), reason3);
});
- test('isInstanceOf', () {
- shouldFail(0, const isInstanceOf<String>(),
- "Expected: an instance of String Actual: <0>");
- shouldPass('cow', const isInstanceOf<String>());
- });
-
group('Predicate Matchers', () {
test('isInstanceOf', () {
shouldFail(0, predicate((x) => x is String, "an instance of String"),
diff --git a/test/having_test.dart b/test/having_test.dart
new file mode 100644
index 0000000..31cf7ac
--- /dev/null
+++ b/test/having_test.dart
@@ -0,0 +1,91 @@
+// Copyright (c) 2018, 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.
+
+import 'package:matcher/matcher.dart';
+import 'package:test/test.dart' show test, expect, throwsA, group;
+
+import 'test_utils.dart';
+
+void main() {
+ test('success', () {
+ shouldPass(new RangeError('details'), _rangeMatcher);
+ });
+
+ test('failure', () {
+ shouldFail(
+ new RangeError.range(-1, 1, 10),
+ _rangeMatcher,
+ "Expected: <Instance of 'RangeError'> with "
+ "`message`: contains 'details' and `start`: null and `end`: null "
+ 'Actual: RangeError:<RangeError: '
+ 'Invalid value: Not in range 1..10, inclusive: -1> '
+ "Which: has `message` with value 'Invalid value'");
+ });
+
+ // This code is used in the [TypeMatcher] doc comments.
+ test('integaration and example', () {
+ void shouldThrowRangeError(int value) {
+ throw new RangeError.range(value, 10, 20);
+ }
+
+ expect(
+ () => shouldThrowRangeError(5),
+ throwsA(const TypeMatcher<RangeError>()
+ .having((e) => e.start, 'start', greaterThanOrEqualTo(10))
+ .having((e) => e.end, 'end', lessThanOrEqualTo(20))));
+
+ expect(
+ () => shouldThrowRangeError(5),
+ throwsA(isRangeError
+ .having((e) => e.start, 'start', greaterThanOrEqualTo(10))
+ .having((e) => e.end, 'end', lessThanOrEqualTo(20))));
+ });
+
+ group('CustomMater copy', () {
+ test("Feature Matcher", () {
+ var w = new Widget();
+ w.price = 10;
+ shouldPass(w, _hasPrice(10));
+ shouldPass(w, _hasPrice(greaterThan(0)));
+ shouldFail(
+ w,
+ _hasPrice(greaterThan(10)),
+ "Expected: <Instance of 'Widget'> with `price`: a value greater than <10> "
+ "Actual: <Instance of 'Widget'> "
+ "Which: has `price` with value <10> which is not "
+ "a value greater than <10>");
+ });
+
+ test("Custom Matcher Exception", () {
+ shouldFail(
+ 'a',
+ _badCustomMatcher(),
+ allOf([
+ contains(
+ "Expected: <Instance of 'Widget'> with `feature`: {1: 'a'} "),
+ contains("Actual: 'a'"),
+ ]));
+ shouldFail(
+ new Widget(),
+ _badCustomMatcher(),
+ allOf([
+ contains(
+ "Expected: <Instance of 'Widget'> with `feature`: {1: 'a'} "),
+ contains("Actual: <Instance of 'Widget'> "),
+ contains("Which: threw 'Exception: bang' "),
+ ]));
+ });
+ });
+}
+
+final _rangeMatcher = isRangeError
+ .having((e) => e.message, 'message', contains('details'))
+ .having((e) => e.start, 'start', isNull)
+ .having((e) => e.end, 'end', isNull);
+
+Matcher _hasPrice(matcher) =>
+ const TypeMatcher<Widget>().having((e) => e.price, 'price', matcher);
+
+Matcher _badCustomMatcher() => const TypeMatcher<Widget>()
+ .having((e) => throw new Exception("bang"), 'feature', {1: "a"});
diff --git a/test/type_matcher_test.dart b/test/type_matcher_test.dart
index c15562b..e20294f 100644
--- a/test/type_matcher_test.dart
+++ b/test/type_matcher_test.dart
@@ -26,8 +26,10 @@
_test('NullThrownError', isNullThrownError, new NullThrownError());
group('custom `TypeMatcher`', () {
+ // ignore: deprecated_member_use
_test('String', const isInstanceOf<String>(), 'hello');
_test('String', const _StringMatcher(), 'hello');
+ _test('String', const TypeMatcher<String>(), 'hello');
});
}
@@ -41,27 +43,21 @@
}
test('fails', () {
- shouldFail(
- const _TestType(),
- typeMatcher,
- anyOf(
- // Handles the TypeMatcher case
- equalsIgnoringWhitespace('Expected: $name Actual: ?:<TestType>'),
- // Handles the `isInstanceOf` case
- equalsIgnoringWhitespace(
- 'Expected: an instance of $name Actual: ?:<TestType>')));
+ shouldFail(const TestType(), typeMatcher,
+ "Expected: <Instance of '$name'> Actual: <Instance of 'TestType'>");
});
});
}
+// Validate that existing implementations continue to work.
class _StringMatcher extends TypeMatcher {
- const _StringMatcher() : super('String');
+ const _StringMatcher() : super(
+ // ignore: deprecated_member_use
+ 'String');
bool matches(item, Map matchState) => item is String;
}
-class _TestType {
- const _TestType();
-
- String toString() => 'TestType';
+class TestType {
+ const TestType();
}