diff --git a/packages/mocktail/lib/src/_invocation_matcher.dart b/packages/mocktail/lib/src/_invocation_matcher.dart index 1895536..9cd8eb7 100644 --- a/packages/mocktail/lib/src/_invocation_matcher.dart +++ b/packages/mocktail/lib/src/_invocation_matcher.dart @@ -62,14 +62,29 @@ class InvocationMatcher { roleInvocation.namedArguments.length) { return false; } - var index = 0; + if (invocation.typeArguments.length != + roleInvocation.typeArguments.length) { + return false; + } + + var positionalArgIndex = 0; for (final roleArg in roleInvocation.positionalArguments) { - final dynamic actArg = invocation.positionalArguments[index]; + final dynamic actArg = invocation.positionalArguments[positionalArgIndex]; if (!_isMatchingArg(roleArg, actArg)) { return false; } - index++; + positionalArgIndex++; } + + var typeArgIndex = 0; + for (final roleArg in roleInvocation.typeArguments) { + final dynamic actArg = invocation.typeArguments[typeArgIndex]; + if (!_isMatchingTypeArg(roleArg, actArg)) { + return false; + } + typeArgIndex++; + } + Set roleKeys = roleInvocation.namedArguments.keys.toSet(); Set actKeys = invocation.namedArguments.keys.toSet(); if (roleKeys.difference(actKeys).isNotEmpty || @@ -93,4 +108,8 @@ class InvocationMatcher { return equals(roleArg).matches(actArg, {}); } } + + bool _isMatchingTypeArg(Type roleTypeArg, dynamic actTypeArg) { + return roleTypeArg == actTypeArg; + } } diff --git a/packages/mocktail/lib/src/_is_invocation.dart b/packages/mocktail/lib/src/_is_invocation.dart index cba2fbc..0837170 100644 --- a/packages/mocktail/lib/src/_is_invocation.dart +++ b/packages/mocktail/lib/src/_is_invocation.dart @@ -17,11 +17,14 @@ class _InvocationMatcher implements Matcher { } return d; } - // For a method, return (). - d = d - .add(_symbolToString(invocation.memberName)) - .add('(') - .addAll('', ', ', '', invocation.positionalArguments); + // For a method, return <>(). + d = d.add(_symbolToString(invocation.memberName)); + + if (invocation.typeArguments.isNotEmpty) { + d.add('<').addAll('', ', ', '', invocation.typeArguments).add('>'); + } + + d.add('(').addAll('', ', ', '', invocation.positionalArguments); if (invocation.positionalArguments.isNotEmpty && invocation.namedArguments.isNotEmpty) { d = d.add(', '); @@ -62,6 +65,8 @@ class _InvocationMatcher implements Matcher { _invocation.memberName == item.memberName && _invocation.isSetter == item.isSetter && _invocation.isGetter == item.isGetter && + const ListEquality(_MatcherEquality()) + .equals(_invocation.typeArguments, item.typeArguments) && const ListEquality(_MatcherEquality()) .equals(_invocation.positionalArguments, item.positionalArguments) && const MapEquality(values: _MatcherEquality()) @@ -113,6 +118,7 @@ class _InvocationForMatchedArguments extends Invocation { this.memberName, this.positionalArguments, this.namedArguments, + this.typeArguments, this.isGetter, this.isMethod, this.isSetter, @@ -134,6 +140,7 @@ class _InvocationForMatchedArguments extends Invocation { invocation.memberName, positionalArguments, namedArguments, + invocation.typeArguments, invocation.isGetter, invocation.isMethod, invocation.isSetter, @@ -147,6 +154,8 @@ class _InvocationForMatchedArguments extends Invocation { @override final List positionalArguments; @override + final List typeArguments; + @override final bool isGetter; @override final bool isMethod; diff --git a/packages/mocktail/lib/src/_real_call.dart b/packages/mocktail/lib/src/_real_call.dart index c8952ad..fab92ed 100644 --- a/packages/mocktail/lib/src/_real_call.dart +++ b/packages/mocktail/lib/src/_real_call.dart @@ -75,7 +75,12 @@ extension on Invocation { var method = _symbolToString(memberName); if (isMethod) { - method = '$method($argString)'; + var typeArgsString = ''; + if (typeArguments.isNotEmpty) { + typeArgsString = '<${typeArguments.join(', ')}>'; + } + + method = '$method$typeArgsString($argString)'; } else if (isGetter) { method = '$method'; } else if (isSetter) { diff --git a/packages/mocktail/test/mockito_compat/invocation_matcher_test.dart b/packages/mocktail/test/mockito_compat/invocation_matcher_test.dart index aabd9a9..b79afc0 100644 --- a/packages/mocktail/test/mockito_compat/invocation_matcher_test.dart +++ b/packages/mocktail/test/mockito_compat/invocation_matcher_test.dart @@ -52,6 +52,25 @@ void main() { ); }); + test('type arguments', () { + stub.promotesTheUprisingOfTheWorkingClass(); + var call1 = Stub.lastInvocation; + stub.promotesTheUprisingOfTheWorkingClass(); + var call2 = Stub.lastInvocation; + stub.promotesTheUprisingOfTheWorkingClass(); + var call3 = Stub.lastInvocation; + shouldPass(call1, isInvocation(call2)); + + shouldFail( + call1, + isInvocation(call3), + 'Expected: promotesTheUprisingOfTheWorkingClass>() ' + "Actual: " + 'Which: Does not match promotesTheUprisingOfTheWorkingClass' + '>()', + ); + }); + test('optional arguments', () { stub.lie(true); var call1 = Stub.lastInvocation; @@ -123,11 +142,19 @@ void main() { abstract class Interface { bool? get value; + set value(bool? value); + void say(String text); + void eat(String food, {bool? alsoDrink}); + void lie([bool? facingDown]); + void fly({int? miles}); + + void promotesTheUprisingOfTheWorkingClass(); + bool? property; } @@ -138,6 +165,7 @@ class MockDescription extends Mock implements Description {} /// Any call always returns an [Invocation]. class Stub implements Interface { const Stub(); + static late Invocation lastInvocation; @override diff --git a/packages/mocktail/test/mockito_compat/until_called_test.dart b/packages/mocktail/test/mockito_compat/until_called_test.dart index e4860c4..d0e9153 100644 --- a/packages/mocktail/test/mockito_compat/until_called_test.dart +++ b/packages/mocktail/test/mockito_compat/until_called_test.dart @@ -11,6 +11,8 @@ class _RealClass { String? methodWithNamedArgs(int? x, {int? y}) => 'Real'; String? methodWithTwoNamedArgs(int? x, {int? y, int? z}) => 'Real'; String? methodWithObjArgs(_RealClass? x) => 'Real'; + String? methodWithTypeArgs() => 'Real'; + String? methodWithDefaultTypeArg() => 'Real'; String? typeParameterizedFn(List? w, List? x, [List? y, List? z]) => 'Real'; @@ -45,6 +47,8 @@ class _RealClassController { ..methodWithNamedArgs(1, y: 2) ..methodWithTwoNamedArgs(1, y: 2, z: 3) ..methodWithObjArgs(_RealClass()) + ..methodWithTypeArgs>() + ..methodWithDefaultTypeArg() ..typeParameterizedFn([1, 2], [3, 4], [5, 6], [7, 8]) ..typeParameterizedNamedFn([1, 2], [3, 4], y: [5, 6], z: [7, 8]) ..getter @@ -136,6 +140,22 @@ void main() { verify(() => mock.methodWithObjArgs(any())).called(1); }); + test('waits for method with type args', () async { + mock.methodWithTypeArgs>(); + + await untilCalled(() => mock.methodWithTypeArgs>()); + + verify(() => mock.methodWithTypeArgs>()).called(1); + }); + + test('waits for method with default type args', () async { + mock.methodWithDefaultTypeArg(); + + await untilCalled(() => mock.methodWithDefaultTypeArg()); + + verify(() => mock.methodWithDefaultTypeArg()).called(1); + }); + test('waits for function with positional parameters', () async { mock.typeParameterizedFn([1, 2], [3, 4], [5, 6], [7, 8]); @@ -264,6 +284,24 @@ void main() { verify(() => mock.methodWithObjArgs(any())).called(1); }); + test('waits for method with type args', () async { + streamController.add(CallMethodsEvent()); + verifyNever(() => mock.methodWithTypeArgs>()); + + await untilCalled(() => mock.methodWithTypeArgs>()); + + verify(() => mock.methodWithTypeArgs>()).called(1); + }); + + test('waits for method with default type args', () async { + streamController.add(CallMethodsEvent()); + verifyNever(() => mock.methodWithDefaultTypeArg()); + + await untilCalled(() => mock.methodWithDefaultTypeArg()); + + verify(() => mock.methodWithDefaultTypeArg()).called(1); + }); + test('waits for function with positional parameters', () async { streamController.add(CallMethodsEvent()); verifyNever(() => mock.typeParameterizedFn(any(), any(), any(), any())); diff --git a/packages/mocktail/test/mockito_compat/verify_test.dart b/packages/mocktail/test/mockito_compat/verify_test.dart index bce86bc..8fa8069 100644 --- a/packages/mocktail/test/mockito_compat/verify_test.dart +++ b/packages/mocktail/test/mockito_compat/verify_test.dart @@ -12,6 +12,8 @@ class _RealClass { String? methodWithPositionalArgs(int? x, [int? y]) => 'Real'; String? methodWithNamedArgs(int x, {int? y}) => 'Real'; String? methodWithOnlyNamedArgs({int? y = 0, int? z}) => 'Real'; + String? methodWithTypeArgs(int? x) => 'Real'; + String? methodWithDefaultTypeArg() => 'Real'; String? get getter => 'Real'; String get nsGetter => 'Real'; set setter(String arg) { @@ -107,6 +109,28 @@ void main() { verify(() => mock.methodWithNamedArgs(42, y: 17)); }); + test('should verify method with type args', () { + mock.methodWithTypeArgs>(42); + expectFail( + 'No matching calls. All calls: ' + '_MockedClass.methodWithTypeArgs>(42)\n' + '$noMatchingCallsFooter', () { + verify(() => mock.methodWithTypeArgs>(42)); + }); + verify(() => mock.methodWithTypeArgs>(42)); + }); + + test('should verify method with default type args', () { + mock.methodWithDefaultTypeArg(); + expectFail( + 'No matching calls. All calls: ' + '_MockedClass.methodWithDefaultTypeArg()\n' + '$noMatchingCallsFooter', () { + verify(() => mock.methodWithDefaultTypeArg()); + }); + verify(() => mock.methodWithDefaultTypeArg()); + }); + test('should mock method with list args', () { mock.methodWithListArgs([42]); expectFail(