Skip to content

Commit

Permalink
Allow mixing types into generated mocks.
Browse files Browse the repository at this point in the history
This is primarily here to support private field promotion: dart-lang/language#2020

Also discussion at dart-lang/language#2275

The broad stroke is that users may need to start declaring little mixins next to their base classes with implementations for this or that private API which is (intentionally or not) accessed against a mock instance during a test.

Fixes dart-lang/mockito#342

PiperOrigin-RevId: 461933542
  • Loading branch information
srawlins committed Jul 28, 2022
1 parent 2f7617b commit 3145e11
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 19 deletions.
6 changes: 5 additions & 1 deletion pkgs/mockito/lib/annotations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ class GenerateMocks {
class MockSpec<T> {
final Symbol? mockName;

final List<Type> mixins;

final bool returnNullOnMissingStub;

final Set<Symbol> unsupportedMembers;
Expand Down Expand Up @@ -103,8 +105,10 @@ class MockSpec<T> {
/// as a legal return value.
const MockSpec({
Symbol? as,
List<Type> mixingIn = const [],
this.returnNullOnMissingStub = false,
this.unsupportedMembers = const {},
this.fallbackGenerators = const {},
}) : mockName = as;
}) : mockName = as,
mixins = mixingIn;
}
41 changes: 39 additions & 2 deletions pkgs/mockito/lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,16 @@ $rawOutput
.whereType<analyzer.InterfaceType>()
.forEach(addTypesFrom);
// For a type like `Foo extends Bar<Baz>`, add the `Baz`.
for (var supertype in type.allSupertypes) {
for (final supertype in type.allSupertypes) {
addTypesFrom(supertype);
}
}

for (var mockTarget in mockTargets) {
for (final mockTarget in mockTargets) {
addTypesFrom(mockTarget.classType);
for (final mixinTarget in mockTarget.mixins) {
addTypesFrom(mixinTarget);
}
}

final typeUris = <Element, String>{};
Expand Down Expand Up @@ -359,6 +362,8 @@ class _MockTarget {
/// The desired name of the mock class.
final String mockName;

final List<analyzer.InterfaceType> mixins;

final bool returnNullOnMissingStub;

final Set<String> unsupportedMembers;
Expand All @@ -368,6 +373,7 @@ class _MockTarget {
_MockTarget(
this.classType,
this.mockName, {
required this.mixins,
required this.returnNullOnMissingStub,
required this.unsupportedMembers,
required this.fallbackGenerators,
Expand Down Expand Up @@ -453,6 +459,7 @@ class _MockTargetGatherer {
mockTargets.add(_MockTarget(
declarationType,
mockName,
mixins: [],
returnNullOnMissingStub: false,
unsupportedMembers: {},
fallbackGenerators: {},
Expand Down Expand Up @@ -480,6 +487,27 @@ class _MockTargetGatherer {
}
final mockName = mockSpec.getField('mockName')!.toSymbolValue() ??
'Mock${type.element.name}';
final mixins = <analyzer.InterfaceType>[];
for (final m in mockSpec.getField('mixins')!.toListValue()!) {
final typeToMixin = m.toTypeValue();
if (typeToMixin == null) {
throw InvalidMockitoAnnotationException(
'The "mixingIn" argument includes a non-type: $m');
}
if (typeToMixin.isDynamic) {
throw InvalidMockitoAnnotationException(
'Mockito cannot mix `dynamic` into a mock class');
}
final mixinInterfaceType =
_determineDartType(typeToMixin, entryLib.typeProvider);
if (!mixinInterfaceType.interfaces.contains(type)) {
throw InvalidMockitoAnnotationException('The "mixingIn" type, '
'${typeToMixin.getDisplayString(withNullability: false)}, must '
'implement the class to mock, ${typeToMock.getDisplayString(withNullability: false)}');
}
mixins.add(mixinInterfaceType);
}

final returnNullOnMissingStub =
mockSpec.getField('returnNullOnMissingStub')!.toBoolValue()!;
final unsupportedMembers = {
Expand All @@ -492,6 +520,7 @@ class _MockTargetGatherer {
mockTargets.add(_MockTarget(
type,
mockName,
mixins: mixins,
returnNullOnMissingStub: returnNullOnMissingStub,
unsupportedMembers: unsupportedMembers,
fallbackGenerators:
Expand Down Expand Up @@ -930,6 +959,14 @@ class _MockClassInfo {
typeArguments.add(refer(typeParameter.name));
}
}
for (final mixin in mockTarget.mixins) {
cBuilder.mixins.add(TypeReference((b) {
b
..symbol = mixin.name
..url = _typeImport(mixin.element)
..types.addAll(mixin.typeArguments.map(_typeReference));
}));
}
cBuilder.implements.add(TypeReference((b) {
b
..symbol = classToMock.name
Expand Down
135 changes: 133 additions & 2 deletions pkgs/mockito/test/builder/custom_mocks_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class GenerateMocks {
class MockSpec<T> {
final Symbol mockName;
final List<Type> mixins;
final bool returnNullOnMissingStub;
final Set<Symbol> unsupportedMembers;
Expand All @@ -43,10 +45,12 @@ class MockSpec<T> {
const MockSpec({
Symbol? as,
List<Type> mixingIn = const [],
this.returnNullOnMissingStub = false,
this.unsupportedMembers = const {},
this.fallbackGenerators = const {},
}) : mockName = as;
}) : mockName = as,
mixins = mixingIn;
}
'''
};
Expand Down Expand Up @@ -95,7 +99,7 @@ void main() {
var packageConfig = PackageConfig([
Package('foo', Uri.file('/foo/'),
packageUriRoot: Uri.file('/foo/lib/'),
languageVersion: LanguageVersion(2, 12))
languageVersion: LanguageVersion(2, 15))
]);
await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
writer: writer, packageConfig: packageConfig);
Expand Down Expand Up @@ -310,6 +314,74 @@ void main() {
contains('class MockBFoo extends _i1.Mock implements _i3.Foo'));
});

test('generates a mock class with a declared mixin', () async {
var mocksContent = await buildWithNonNullable({
...annotationsAsset,
'foo|lib/foo.dart': dedent('''
class Foo {}
class FooMixin implements Foo {}
'''),
'foo|test/foo_test.dart': '''
import 'package:foo/foo.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([], customMocks: [MockSpec<Foo>(mixingIn: [FooMixin])])
void main() {}
'''
});
expect(
mocksContent,
contains(
'class MockFoo extends _i1.Mock with _i2.FooMixin implements _i2.Foo {'),
);
});

test('generates a mock class with multiple declared mixins', () async {
var mocksContent = await buildWithNonNullable({
...annotationsAsset,
'foo|lib/foo.dart': dedent('''
class Foo {}
class Mixin1 implements Foo {}
class Mixin2 implements Foo {}
'''),
'foo|test/foo_test.dart': '''
import 'package:foo/foo.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([], customMocks: [MockSpec<Foo>(mixingIn: [Mixin1, Mixin2])])
void main() {}
'''
});
expect(
mocksContent,
contains(
'class MockFoo extends _i1.Mock with _i2.Mixin1, _i2.Mixin2 implements _i2.Foo {'),
);
});

test('generates a mock class with a declared mixin with a type arg',
() async {
var mocksContent = await buildWithNonNullable({
...annotationsAsset,
'foo|lib/foo.dart': dedent('''
class Foo<T> {}
class FooMixin<T> implements Foo<T> {}
'''),
'foo|test/foo_test.dart': '''
import 'package:foo/foo.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([], customMocks: [MockSpec<Foo<int>>(mixingIn: [FooMixin<int>])])
void main() {}
'''
});
expect(
mocksContent,
contains(
'class MockFoo extends _i1.Mock with _i2.FooMixin<int> implements _i2.Foo<int> {'),
);
});

test(
'generates a mock class which uses the old behavior of returning null on '
'missing stubs', () async {
Expand Down Expand Up @@ -804,6 +876,65 @@ void main() {
);
});

test('throws when MockSpec mixes in dynamic', () async {
_expectBuilderThrows(
assets: {
...annotationsAsset,
'foo|lib/foo.dart': dedent('''
class Foo {}
'''),
'foo|test/foo_test.dart': dedent('''
import 'package:mockito/annotations.dart';
import 'package:foo/foo.dart';
@GenerateMocks([], customMocks: [MockSpec<Foo>(mixingIn: [dynamic])])
void main() {}
'''),
},
message: contains('Mockito cannot mix `dynamic` into a mock class'),
);
});

test('throws when MockSpec mixes in a private type', () async {
_expectBuilderThrows(
assets: {
...annotationsAsset,
'foo|lib/foo.dart': dedent('''
class Foo {}
'''),
'foo|test/foo_test.dart': dedent('''
import 'package:mockito/annotations.dart';
import 'package:foo/foo.dart';
@GenerateMocks([], customMocks: [MockSpec<Foo>(mixingIn: [_FooMixin])])
void main() {}
mixin _FooMixin implements Foo {}
'''),
},
message: contains('Mockito cannot mock a private type: _FooMixin'),
);
});

test('throws when MockSpec mixes in a non-mixinable type', () async {
_expectBuilderThrows(
assets: {
...annotationsAsset,
'foo|lib/foo.dart': dedent('''
class Foo {}
'''),
'foo|test/foo_test.dart': dedent('''
import 'package:mockito/annotations.dart';
import 'package:foo/foo.dart';
@GenerateMocks([], customMocks: [MockSpec<Foo>(mixingIn: [FooMixin])])
void main() {}
mixin FooMixin {}
'''),
},
message: contains(
'The "mixingIn" type, FooMixin, must implement the class to mock, Foo'),
);
});

test('given a pre-non-nullable library, does not override any members',
() async {
await testPreNonNullable(
Expand Down
15 changes: 15 additions & 0 deletions pkgs/mockito/test/end2end/foo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,18 @@ abstract class Baz<S> {
S Function(S) returnsGenericFunction();
S get typeVariableField;
}

class HasPrivate {
Object? _p;

Object? get p => _p;
}

void setPrivate(HasPrivate hasPrivate) {
hasPrivate._p = 7;
}

mixin HasPrivateMixin implements HasPrivate {
@override
Object? _p;
}
42 changes: 28 additions & 14 deletions pkgs/mockito/test/end2end/generated_mocks_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,27 @@ T Function(T) returnsGenericFunctionShim<T>() => (T _) => null as T;
], customMocks: [
MockSpec<Foo>(as: #MockFooRelaxed, returnNullOnMissingStub: true),
MockSpec<Bar>(as: #MockBarRelaxed, returnNullOnMissingStub: true),
MockSpec<Baz>(as: #MockBazWithUnsupportedMembers, unsupportedMembers: {
#returnsTypeVariable,
#returnsBoundedTypeVariable,
#returnsTypeVariableFromTwo,
#returnsGenericFunction,
#typeVariableField,
}),
MockSpec<Baz>(as: #MockBazWithFallbackGenerators, fallbackGenerators: {
#returnsTypeVariable: returnsTypeVariableShim,
#returnsBoundedTypeVariable: returnsBoundedTypeVariableShim,
#returnsTypeVariableFromTwo: returnsTypeVariableFromTwoShim,
#returnsGenericFunction: returnsGenericFunctionShim,
#typeVariableField: typeVariableFieldShim,
}),
MockSpec<Baz>(
as: #MockBazWithUnsupportedMembers,
unsupportedMembers: {
#returnsTypeVariable,
#returnsBoundedTypeVariable,
#returnsTypeVariableFromTwo,
#returnsGenericFunction,
#typeVariableField,
},
),
MockSpec<Baz>(
as: #MockBazWithFallbackGenerators,
fallbackGenerators: {
#returnsTypeVariable: returnsTypeVariableShim,
#returnsBoundedTypeVariable: returnsBoundedTypeVariableShim,
#returnsTypeVariableFromTwo: returnsTypeVariableFromTwoShim,
#returnsGenericFunction: returnsGenericFunctionShim,
#typeVariableField: typeVariableFieldShim,
},
),
MockSpec<HasPrivate>(mixingIn: [HasPrivateMixin]),
])
void main() {
group('for a generated mock,', () {
Expand Down Expand Up @@ -257,4 +264,11 @@ void main() {
when(foo.methodWithBarArg(bar)).thenReturn('mocked result');
expect(foo.methodWithBarArg(bar), equals('mocked result'));
});

test('a generated mock with a mixed in type can use mixed in members', () {
var hasPrivate = MockHasPrivate();
// This should not throw, when `setPrivate` accesses a private member on
// `hasPrivate`.
setPrivate(hasPrivate);
});
}

0 comments on commit 3145e11

Please sign in to comment.