Support nSM Forwarding (#133)
Support nSM Forwarding
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49ce5db..fc74b07 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,52 @@
+## 3.0.0-alpha+5
+
+* Fix compatibility with new [noSuchMethod Forwarding] feature of Dart 2. This
+ is thankfully a mostly backwards-compatible change. This means that this
+ version of Mockito should continue to work:
+
+ * with Dart `>=2.0.0-dev.16.0`,
+ * with Dart 2 runtime semantics (i.e. with `dart --preview-dart-2`, or with
+ Flutter Beta 3), and
+ * with the new noSuchMethod Forwarding feature, when it lands in CFE, and when
+ it lands in DDC.
+
+ This change, when combined with noSuchMethod Forwarding, will break a few
+ code paths which do not seem to be frequently used. Two examples:
+
+ ```dart
+ class A {
+ int fn(int a, [int b]) => 7;
+ }
+ class MockA extends Mock implements A {}
+
+ var a = new MockA();
+ when(a.fn(typed(any), typed(any))).thenReturn(0);
+ print(a.fn(1));
+ ```
+
+ This used to print `null`, because only one argument was passed, which did
+ not match the two-argument stub. Now it will print `0`, as the real call
+ contains a value for both the required argument, and the optional argument.
+
+ ```dart
+ a.fn(1);
+ a.fn(2, 3);
+ print(verify(a.fn(typed(captureAny), typed(captureAny))).captured);
+ ```
+
+ This used to print `[2, 3]`, because only the second call matched the `verify`
+ call. Now, it will print `[1, null, 2, 3]`, as both real calls contain a value
+ for both the required argument, and the optional argument.
+
+[noSuchMethod Forwarding]: https://github.com/dart-lang/sdk/blob/master/docs/language/informal/nosuchmethod-forwarding.md
+
+## 3.0.0-alpha+4
+
+* Introduce a backward-and-forward compatible API to help users migrate to
+ Mockito 3. See more details in the [upgrading-to-mockito-3] doc.
+
+[upgrading-to-mockito-3]: https://github.com/dart-lang/mockito/blob/master/upgrading-to-mockito-3.md
+
## 3.0.0-alpha+3
* `thenReturn` and `thenAnswer` now support generics and infer the correct
diff --git a/lib/src/mock.dart b/lib/src/mock.dart
index b8f6ab5..34978be 100644
--- a/lib/src/mock.dart
+++ b/lib/src/mock.dart
@@ -215,20 +215,10 @@
invocation.namedArguments.forEach((name, arg) {
if (arg == null) {
if (!_storedNamedArgSymbols.contains(name)) {
- // Incorrect usage of an ArgMatcher, something like:
- // `when(obj.fn(a: any))`.
-
- // Clear things out for the next call.
- _storedArgs.clear();
- _storedNamedArgs.clear();
- throw new ArgumentError(
- 'An ArgumentMatcher (or a null value) was passed in as a named '
- 'argument named "$name", but was not passed a value for `named`. '
- 'Each ArgumentMatcher that is passed as a named argument needs '
- 'to specify the `named` argument, and each null value must be '
- 'wrapped in an ArgMatcher. For example: '
- '`when(obj.fn(x: anyNamed("x")))` or '
- '`when(obj.fn(x: argThat(isNull, named: "x")))`.');
+ // Either this is a parameter with default value `null`, or a `null`
+ // argument was passed, or an unnamed ArgMatcher was used. Just use
+ // `null`.
+ namedArguments[name] = null;
}
} else {
// Add each real named argument (not wrapped in an ArgMatcher).
@@ -271,13 +261,19 @@
var positionalArguments = <dynamic>[];
var nullPositionalArguments =
invocation.positionalArguments.where((arg) => arg == null);
- if (_storedArgs.length != nullPositionalArguments.length) {
- // Clear things out for the next call.
+ if (_storedArgs.length > nullPositionalArguments.length) {
+ // More _positional_ ArgMatchers were stored than were actually passed as
+ // positional arguments. The only way this call was parsed and resolved is
+ // if an ArgMatcher was passed as a named argument, but without a name,
+ // and thus stored in [_storedArgs], something like
+ // `when(obj.fn(a: any))`.
_storedArgs.clear();
_storedNamedArgs.clear();
throw new ArgumentError(
- 'null arguments are not allowed alongside ArgMatchers; use '
- '"argThat(isNull)"');
+ 'An argument matcher (like `any`) was used as a named argument, but '
+ 'did not use a Mockito "named" API. Each argument matcher that is '
+ 'used as a named argument needs to specify the name of the argument '
+ 'it is being used in. For example: `when(obj.fn(x: anyNamed("x")))`.');
}
int storedIndex = 0;
int positionalIndex = 0;
diff --git a/test/capture_test.dart b/test/capture_test.dart
index 13ab95a..8ca3606 100644
--- a/test/capture_test.dart
+++ b/test/capture_test.dart
@@ -16,6 +16,8 @@
import 'package:mockito/src/mock.dart' show resetMockitoState;
import 'package:test/test.dart';
+import 'utils.dart';
+
class RealClass {
RealClass innerObj;
String methodWithNormalArgs(int x) => 'Real';
@@ -31,6 +33,8 @@
void main() {
MockedClass mock;
+ var isNsmForwarding = assessNsmForwarding();
+
setUp(() {
mock = new MockedClass();
});
@@ -78,11 +82,12 @@
test('should capture with matching arguments', () {
mock.methodWithPositionalArgs(1);
mock.methodWithPositionalArgs(2, 3);
+ var expectedCaptures = isNsmForwarding ? [1, null, 2, 3] : [2, 3];
expect(
verify(mock.methodWithPositionalArgs(
typed(captureAny), typed(captureAny)))
.captured,
- equals([2, 3]));
+ equals(expectedCaptures));
});
test('should capture multiple invocations', () {
diff --git a/test/mockito_test.dart b/test/mockito_test.dart
index 49946d7..5794740 100644
--- a/test/mockito_test.dart
+++ b/test/mockito_test.dart
@@ -19,6 +19,8 @@
show resetMockitoState, throwOnMissingStub;
import 'package:test/test.dart';
+import 'utils.dart';
+
class RealClass {
RealClass innerObj;
String methodWithoutArgs() => "Real";
@@ -72,6 +74,8 @@
void main() {
MockedClass mock;
+ var isNsmForwarding = assessNsmForwarding();
+
setUp(() {
mock = new MockedClass();
});
@@ -153,7 +157,10 @@
.thenReturn("x y");
when(mock.methodWithTwoNamedArgs(any, z: anyNamed('z')))
.thenReturn("x z");
- expect(mock.methodWithTwoNamedArgs(42), isNull);
+ if (isNsmForwarding)
+ expect(mock.methodWithTwoNamedArgs(42), "x z");
+ else
+ expect(mock.methodWithTwoNamedArgs(42), isNull);
expect(mock.methodWithTwoNamedArgs(42, y: 18), equals("x y"));
expect(mock.methodWithTwoNamedArgs(42, z: 17), equals("x z"));
expect(mock.methodWithTwoNamedArgs(42, y: 18, z: 17), isNull);
@@ -252,16 +259,6 @@
}, throwsStateError);
});
- test("should throw if `null` is passed alongside matchers", () {
- expect(() {
- when(mock.methodWithPositionalArgs(argThat(equals(42)), null))
- .thenReturn("99");
- }, throwsArgumentError);
-
- // but doesn't ruin later calls.
- when(mock.methodWithNormalArgs(43)).thenReturn("43");
- });
-
test("thenReturn throws if provided Future", () {
expect(
() => when(mock.methodReturningFuture())
@@ -290,20 +287,6 @@
expect(await mock.methodReturningStream().toList(), ["stub"]);
});
- test("should throw if `null` is passed as a named arg", () {
- expect(() {
- when(mock.methodWithNamedArgs(argThat(equals(42)), y: null))
- .thenReturn("99");
- }, throwsArgumentError);
- });
-
- test("should throw if named matcher is passed as a positional arg", () {
- expect(() {
- when(mock.methodWithNamedArgs(argThat(equals(42), named: "y")))
- .thenReturn("99");
- }, throwsArgumentError);
- });
-
test("should throw if named matcher is passed as the wrong name", () {
expect(() {
when(mock.methodWithNamedArgs(argThat(equals(42)), y: anyNamed("z")))
diff --git a/test/utils.dart b/test/utils.dart
new file mode 100644
index 0000000..a8ecd49
--- /dev/null
+++ b/test/utils.dart
@@ -0,0 +1,21 @@
+import 'package:mockito/mockito.dart';
+
+abstract class NsmForwardingSignal {
+ void fn([int a]);
+}
+
+class MockNsmForwardingSignal extends Mock implements NsmForwardingSignal {}
+
+bool assessNsmForwarding() {
+ var signal = new MockNsmForwardingSignal();
+ signal.fn();
+ try {
+ verify(signal.fn(typed(any)));
+ return true;
+ } catch (_) {
+ // The verify failed, because the default value of 7 was not passed to
+ // noSuchMethod.
+ verify(signal.fn());
+ return false;
+ }
+}
diff --git a/test/verify_test.dart b/test/verify_test.dart
index 9c1bcf1..6945306 100644
--- a/test/verify_test.dart
+++ b/test/verify_test.dart
@@ -16,6 +16,8 @@
import 'package:mockito/src/mock.dart' show resetMockitoState;
import 'package:test/test.dart';
+import 'utils.dart';
+
class RealClass {
RealClass innerObj;
String methodWithoutArgs() => 'Real';
@@ -73,6 +75,8 @@
void main() {
MockedClass mock;
+ var isNsmForwarding = assessNsmForwarding();
+
setUp(() {
mock = new MockedClass();
});
@@ -242,8 +246,9 @@
test('and there is one unmatched call without args', () {
mock.methodWithOptionalArg();
+ var nsmForwardedArgs = isNsmForwarding ? 'null' : '';
expectFail(
- 'No matching calls. All calls: MockedClass.methodWithOptionalArg()\n'
+ 'No matching calls. All calls: MockedClass.methodWithOptionalArg($nsmForwardedArgs)\n'
'$noMatchingCallsFooter', () {
verify(mock.methodWithOptionalArg(43));
});
@@ -263,9 +268,10 @@
test('and unmatched calls have only named args', () {
mock.methodWithOnlyNamedArgs(y: 1);
+ var nsmForwardedArgs = isNsmForwarding ? '{y: 1, z: null}' : '{y: 1}';
expectFail(
'No matching calls. All calls: '
- 'MockedClass.methodWithOnlyNamedArgs({y: 1})\n'
+ 'MockedClass.methodWithOnlyNamedArgs($nsmForwardedArgs)\n'
'$noMatchingCallsFooter', () {
verify(mock.methodWithOnlyNamedArgs());
});
@@ -509,6 +515,8 @@
mock.methodWithLongArgs(null, null,
c: new LongToString([5, 6], {5: 'g', 6: 'h'}, 'i'),
d: new LongToString([7, 8], {7: 'j', 8: 'k'}, 'l'));
+ var nsmForwardedNamedArgs =
+ isNsmForwarding ? '>, {c: null, d: null}),' : '>),';
expectFail(
'No matching calls. All calls: '
'MockedClass.methodWithLongArgs(\n'
@@ -521,7 +529,7 @@
' aList: [4, 5]\n'
' aMap: {3: d, 4: e}\n'
' aString: f\n'
- ' >),\n'
+ ' $nsmForwardedNamedArgs\n'
'MockedClass.methodWithLongArgs(null, null, {\n'
' c: LongToString<\n'
' aList: [5, 6]\n'