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

feat!: support type args #67

Merged
merged 6 commits into from Dec 20, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 22 additions & 3 deletions packages/mocktail/lib/src/_invocation_matcher.dart
Expand Up @@ -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 ||
Expand All @@ -93,4 +108,8 @@ class InvocationMatcher {
return equals(roleArg).matches(actArg, <dynamic, dynamic>{});
}
}

bool _isMatchingTypeArg(Type roleTypeArg, dynamic actTypeArg) {
return roleTypeArg == actTypeArg;
}
}
19 changes: 14 additions & 5 deletions packages/mocktail/lib/src/_is_invocation.dart
Expand Up @@ -17,11 +17,14 @@ class _InvocationMatcher implements Matcher {
}
return d;
}
// For a method, return <member>(<args>).
d = d
.add(_symbolToString(invocation.memberName))
.add('(')
.addAll('', ', ', '', invocation.positionalArguments);
// For a method, return <member><<typeArgs>>(<args>).
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(', ');
Expand Down Expand Up @@ -62,6 +65,8 @@ class _InvocationMatcher implements Matcher {
_invocation.memberName == item.memberName &&
_invocation.isSetter == item.isSetter &&
_invocation.isGetter == item.isGetter &&
const ListEquality<dynamic>(_MatcherEquality())
.equals(_invocation.typeArguments, item.typeArguments) &&
const ListEquality<dynamic>(_MatcherEquality())
.equals(_invocation.positionalArguments, item.positionalArguments) &&
const MapEquality<dynamic, dynamic>(values: _MatcherEquality())
Expand Down Expand Up @@ -113,6 +118,7 @@ class _InvocationForMatchedArguments extends Invocation {
this.memberName,
this.positionalArguments,
this.namedArguments,
this.typeArguments,
this.isGetter,
this.isMethod,
this.isSetter,
Expand All @@ -134,6 +140,7 @@ class _InvocationForMatchedArguments extends Invocation {
invocation.memberName,
positionalArguments,
namedArguments,
invocation.typeArguments,
invocation.isGetter,
invocation.isMethod,
invocation.isSetter,
Expand All @@ -147,6 +154,8 @@ class _InvocationForMatchedArguments extends Invocation {
@override
final List<dynamic> positionalArguments;
@override
final List<Type> typeArguments;
@override
final bool isGetter;
@override
final bool isMethod;
Expand Down
7 changes: 6 additions & 1 deletion packages/mocktail/lib/src/_real_call.dart
Expand Up @@ -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) {
Expand Down
Expand Up @@ -52,6 +52,25 @@ void main() {
);
});

test('type arguments', () {
stub.promotesTheUprisingOfTheWorkingClass<int>();
var call1 = Stub.lastInvocation;
stub.promotesTheUprisingOfTheWorkingClass<int>();
var call2 = Stub.lastInvocation;
stub.promotesTheUprisingOfTheWorkingClass();
var call3 = Stub.lastInvocation;
shouldPass(call1, isInvocation(call2));

shouldFail(
call1,
isInvocation(call3),
'Expected: promotesTheUprisingOfTheWorkingClass<Type:<num>>() '
"Actual: <Instance of '${call3.runtimeType}'> "
'Which: Does not match promotesTheUprisingOfTheWorkingClass'
'<Type:<int>>()',
);
});

test('optional arguments', () {
stub.lie(true);
var call1 = Stub.lastInvocation;
Expand Down Expand Up @@ -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<A extends num>();

bool? property;
}

Expand All @@ -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
Expand Down
38 changes: 38 additions & 0 deletions packages/mocktail/test/mockito_compat/until_called_test.dart
Expand Up @@ -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<T>() => 'Real';
String? methodWithDefaultTypeArg<T extends num>() => 'Real';
String? typeParameterizedFn(List<int>? w, List<int>? x,
[List<int>? y, List<int>? z]) =>
'Real';
Expand Down Expand Up @@ -45,6 +47,8 @@ class _RealClassController {
..methodWithNamedArgs(1, y: 2)
..methodWithTwoNamedArgs(1, y: 2, z: 3)
..methodWithObjArgs(_RealClass())
..methodWithTypeArgs<List<String>>()
..methodWithDefaultTypeArg()
..typeParameterizedFn([1, 2], [3, 4], [5, 6], [7, 8])
..typeParameterizedNamedFn([1, 2], [3, 4], y: [5, 6], z: [7, 8])
..getter
Expand Down Expand Up @@ -136,6 +140,22 @@ void main() {
verify(() => mock.methodWithObjArgs(any())).called(1);
});

test('waits for method with type args', () async {
mock.methodWithTypeArgs<List<String>>();

await untilCalled(() => mock.methodWithTypeArgs<List<String>>());

verify(() => mock.methodWithTypeArgs<List<String>>()).called(1);
});

test('waits for method with default type args', () async {
mock.methodWithDefaultTypeArg();

await untilCalled(() => mock.methodWithDefaultTypeArg());

verify(() => mock.methodWithDefaultTypeArg<num>()).called(1);
});

test('waits for function with positional parameters', () async {
mock.typeParameterizedFn([1, 2], [3, 4], [5, 6], [7, 8]);

Expand Down Expand Up @@ -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<List<String>>());

await untilCalled(() => mock.methodWithTypeArgs<List<String>>());

verify(() => mock.methodWithTypeArgs<List<String>>()).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()));
Expand Down
24 changes: 24 additions & 0 deletions packages/mocktail/test/mockito_compat/verify_test.dart
Expand Up @@ -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<T>(int? x) => 'Real';
String? methodWithDefaultTypeArg<T extends num>() => 'Real';
String? get getter => 'Real';
String get nsGetter => 'Real';
set setter(String arg) {
Expand Down Expand Up @@ -107,6 +109,28 @@ void main() {
verify(() => mock.methodWithNamedArgs(42, y: 17));
});

test('should verify method with type args', () {
mock.methodWithTypeArgs<List<double>>(42);
expectFail(
'No matching calls. All calls: '
'_MockedClass.methodWithTypeArgs<List<double>>(42)\n'
'$noMatchingCallsFooter', () {
verify(() => mock.methodWithTypeArgs<List<String>>(42));
});
verify(() => mock.methodWithTypeArgs<List<double>>(42));
});

test('should verify method with default type args', () {
mock.methodWithDefaultTypeArg();
expectFail(
'No matching calls. All calls: '
'_MockedClass.methodWithDefaultTypeArg<num>()\n'
'$noMatchingCallsFooter', () {
verify(() => mock.methodWithDefaultTypeArg<int>());
});
verify(() => mock.methodWithDefaultTypeArg());
});

test('should mock method with list args', () {
mock.methodWithListArgs([42]);
expectFail(
Expand Down