Skip to content

Commit 3be07de

Browse files
committed
Allow mixing types into generated mocks.
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 #342 PiperOrigin-RevId: 461933542
1 parent 714149a commit 3be07de

File tree

5 files changed

+220
-19
lines changed

5 files changed

+220
-19
lines changed

lib/annotations.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ class GenerateMocks {
6969
class MockSpec<T> {
7070
final Symbol? mockName;
7171

72+
final List<Type> mixins;
73+
7274
final bool returnNullOnMissingStub;
7375

7476
final Set<Symbol> unsupportedMembers;
@@ -103,8 +105,10 @@ class MockSpec<T> {
103105
/// as a legal return value.
104106
const MockSpec({
105107
Symbol? as,
108+
List<Type> mixingIn = const [],
106109
this.returnNullOnMissingStub = false,
107110
this.unsupportedMembers = const {},
108111
this.fallbackGenerators = const {},
109-
}) : mockName = as;
112+
}) : mockName = as,
113+
mixins = mixingIn;
110114
}

lib/src/builder.dart

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,16 @@ $rawOutput
153153
.whereType<analyzer.InterfaceType>()
154154
.forEach(addTypesFrom);
155155
// For a type like `Foo extends Bar<Baz>`, add the `Baz`.
156-
for (var supertype in type.allSupertypes) {
156+
for (final supertype in type.allSupertypes) {
157157
addTypesFrom(supertype);
158158
}
159159
}
160160

161-
for (var mockTarget in mockTargets) {
161+
for (final mockTarget in mockTargets) {
162162
addTypesFrom(mockTarget.classType);
163+
for (final mixinTarget in mockTarget.mixins) {
164+
addTypesFrom(mixinTarget);
165+
}
163166
}
164167

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

365+
final List<analyzer.InterfaceType> mixins;
366+
362367
final bool returnNullOnMissingStub;
363368

364369
final Set<String> unsupportedMembers;
@@ -368,6 +373,7 @@ class _MockTarget {
368373
_MockTarget(
369374
this.classType,
370375
this.mockName, {
376+
required this.mixins,
371377
required this.returnNullOnMissingStub,
372378
required this.unsupportedMembers,
373379
required this.fallbackGenerators,
@@ -453,6 +459,7 @@ class _MockTargetGatherer {
453459
mockTargets.add(_MockTarget(
454460
declarationType,
455461
mockName,
462+
mixins: [],
456463
returnNullOnMissingStub: false,
457464
unsupportedMembers: {},
458465
fallbackGenerators: {},
@@ -480,6 +487,27 @@ class _MockTargetGatherer {
480487
}
481488
final mockName = mockSpec.getField('mockName')!.toSymbolValue() ??
482489
'Mock${type.element.name}';
490+
final mixins = <analyzer.InterfaceType>[];
491+
for (final m in mockSpec.getField('mixins')!.toListValue()!) {
492+
final typeToMixin = m.toTypeValue();
493+
if (typeToMixin == null) {
494+
throw InvalidMockitoAnnotationException(
495+
'The "mixingIn" argument includes a non-type: $m');
496+
}
497+
if (typeToMixin.isDynamic) {
498+
throw InvalidMockitoAnnotationException(
499+
'Mockito cannot mix `dynamic` into a mock class');
500+
}
501+
final mixinInterfaceType =
502+
_determineDartType(typeToMixin, entryLib.typeProvider);
503+
if (!mixinInterfaceType.interfaces.contains(type)) {
504+
throw InvalidMockitoAnnotationException('The "mixingIn" type, '
505+
'${typeToMixin.getDisplayString(withNullability: false)}, must '
506+
'implement the class to mock, ${typeToMock.getDisplayString(withNullability: false)}');
507+
}
508+
mixins.add(mixinInterfaceType);
509+
}
510+
483511
final returnNullOnMissingStub =
484512
mockSpec.getField('returnNullOnMissingStub')!.toBoolValue()!;
485513
final unsupportedMembers = {
@@ -492,6 +520,7 @@ class _MockTargetGatherer {
492520
mockTargets.add(_MockTarget(
493521
type,
494522
mockName,
523+
mixins: mixins,
495524
returnNullOnMissingStub: returnNullOnMissingStub,
496525
unsupportedMembers: unsupportedMembers,
497526
fallbackGenerators:
@@ -930,6 +959,14 @@ class _MockClassInfo {
930959
typeArguments.add(refer(typeParameter.name));
931960
}
932961
}
962+
for (final mixin in mockTarget.mixins) {
963+
cBuilder.mixins.add(TypeReference((b) {
964+
b
965+
..symbol = mixin.name
966+
..url = _typeImport(mixin.element)
967+
..types.addAll(mixin.typeArguments.map(_typeReference));
968+
}));
969+
}
933970
cBuilder.implements.add(TypeReference((b) {
934971
b
935972
..symbol = classToMock.name

test/builder/custom_mocks_test.dart

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class GenerateMocks {
3535
class MockSpec<T> {
3636
final Symbol mockName;
3737
38+
final List<Type> mixins;
39+
3840
final bool returnNullOnMissingStub;
3941
4042
final Set<Symbol> unsupportedMembers;
@@ -43,10 +45,12 @@ class MockSpec<T> {
4345
4446
const MockSpec({
4547
Symbol? as,
48+
List<Type> mixingIn = const [],
4649
this.returnNullOnMissingStub = false,
4750
this.unsupportedMembers = const {},
4851
this.fallbackGenerators = const {},
49-
}) : mockName = as;
52+
}) : mockName = as,
53+
mixins = mixingIn;
5054
}
5155
'''
5256
};
@@ -95,7 +99,7 @@ void main() {
9599
var packageConfig = PackageConfig([
96100
Package('foo', Uri.file('/foo/'),
97101
packageUriRoot: Uri.file('/foo/lib/'),
98-
languageVersion: LanguageVersion(2, 12))
102+
languageVersion: LanguageVersion(2, 15))
99103
]);
100104
await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
101105
writer: writer, packageConfig: packageConfig);
@@ -310,6 +314,74 @@ void main() {
310314
contains('class MockBFoo extends _i1.Mock implements _i3.Foo'));
311315
});
312316

317+
test('generates a mock class with a declared mixin', () async {
318+
var mocksContent = await buildWithNonNullable({
319+
...annotationsAsset,
320+
'foo|lib/foo.dart': dedent('''
321+
class Foo {}
322+
323+
class FooMixin implements Foo {}
324+
'''),
325+
'foo|test/foo_test.dart': '''
326+
import 'package:foo/foo.dart';
327+
import 'package:mockito/annotations.dart';
328+
@GenerateMocks([], customMocks: [MockSpec<Foo>(mixingIn: [FooMixin])])
329+
void main() {}
330+
'''
331+
});
332+
expect(
333+
mocksContent,
334+
contains(
335+
'class MockFoo extends _i1.Mock with _i2.FooMixin implements _i2.Foo {'),
336+
);
337+
});
338+
339+
test('generates a mock class with multiple declared mixins', () async {
340+
var mocksContent = await buildWithNonNullable({
341+
...annotationsAsset,
342+
'foo|lib/foo.dart': dedent('''
343+
class Foo {}
344+
345+
class Mixin1 implements Foo {}
346+
class Mixin2 implements Foo {}
347+
'''),
348+
'foo|test/foo_test.dart': '''
349+
import 'package:foo/foo.dart';
350+
import 'package:mockito/annotations.dart';
351+
@GenerateMocks([], customMocks: [MockSpec<Foo>(mixingIn: [Mixin1, Mixin2])])
352+
void main() {}
353+
'''
354+
});
355+
expect(
356+
mocksContent,
357+
contains(
358+
'class MockFoo extends _i1.Mock with _i2.Mixin1, _i2.Mixin2 implements _i2.Foo {'),
359+
);
360+
});
361+
362+
test('generates a mock class with a declared mixin with a type arg',
363+
() async {
364+
var mocksContent = await buildWithNonNullable({
365+
...annotationsAsset,
366+
'foo|lib/foo.dart': dedent('''
367+
class Foo<T> {}
368+
369+
class FooMixin<T> implements Foo<T> {}
370+
'''),
371+
'foo|test/foo_test.dart': '''
372+
import 'package:foo/foo.dart';
373+
import 'package:mockito/annotations.dart';
374+
@GenerateMocks([], customMocks: [MockSpec<Foo<int>>(mixingIn: [FooMixin<int>])])
375+
void main() {}
376+
'''
377+
});
378+
expect(
379+
mocksContent,
380+
contains(
381+
'class MockFoo extends _i1.Mock with _i2.FooMixin<int> implements _i2.Foo<int> {'),
382+
);
383+
});
384+
313385
test(
314386
'generates a mock class which uses the old behavior of returning null on '
315387
'missing stubs', () async {
@@ -804,6 +876,65 @@ void main() {
804876
);
805877
});
806878

879+
test('throws when MockSpec mixes in dynamic', () async {
880+
_expectBuilderThrows(
881+
assets: {
882+
...annotationsAsset,
883+
'foo|lib/foo.dart': dedent('''
884+
class Foo {}
885+
'''),
886+
'foo|test/foo_test.dart': dedent('''
887+
import 'package:mockito/annotations.dart';
888+
import 'package:foo/foo.dart';
889+
@GenerateMocks([], customMocks: [MockSpec<Foo>(mixingIn: [dynamic])])
890+
void main() {}
891+
'''),
892+
},
893+
message: contains('Mockito cannot mix `dynamic` into a mock class'),
894+
);
895+
});
896+
897+
test('throws when MockSpec mixes in a private type', () async {
898+
_expectBuilderThrows(
899+
assets: {
900+
...annotationsAsset,
901+
'foo|lib/foo.dart': dedent('''
902+
class Foo {}
903+
'''),
904+
'foo|test/foo_test.dart': dedent('''
905+
import 'package:mockito/annotations.dart';
906+
import 'package:foo/foo.dart';
907+
@GenerateMocks([], customMocks: [MockSpec<Foo>(mixingIn: [_FooMixin])])
908+
void main() {}
909+
910+
mixin _FooMixin implements Foo {}
911+
'''),
912+
},
913+
message: contains('Mockito cannot mock a private type: _FooMixin'),
914+
);
915+
});
916+
917+
test('throws when MockSpec mixes in a non-mixinable type', () async {
918+
_expectBuilderThrows(
919+
assets: {
920+
...annotationsAsset,
921+
'foo|lib/foo.dart': dedent('''
922+
class Foo {}
923+
'''),
924+
'foo|test/foo_test.dart': dedent('''
925+
import 'package:mockito/annotations.dart';
926+
import 'package:foo/foo.dart';
927+
@GenerateMocks([], customMocks: [MockSpec<Foo>(mixingIn: [FooMixin])])
928+
void main() {}
929+
930+
mixin FooMixin {}
931+
'''),
932+
},
933+
message: contains(
934+
'The "mixingIn" type, FooMixin, must implement the class to mock, Foo'),
935+
);
936+
});
937+
807938
test('given a pre-non-nullable library, does not override any members',
808939
() async {
809940
await testPreNonNullable(

test/end2end/foo.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,18 @@ abstract class Baz<S> {
3131
S Function(S) returnsGenericFunction();
3232
S get typeVariableField;
3333
}
34+
35+
class HasPrivate {
36+
Object? _p;
37+
38+
Object? get p => _p;
39+
}
40+
41+
void setPrivate(HasPrivate hasPrivate) {
42+
hasPrivate._p = 7;
43+
}
44+
45+
mixin HasPrivateMixin implements HasPrivate {
46+
@override
47+
Object? _p;
48+
}

test/end2end/generated_mocks_test.dart

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,27 @@ T Function(T) returnsGenericFunctionShim<T>() => (T _) => null as T;
2525
], customMocks: [
2626
MockSpec<Foo>(as: #MockFooRelaxed, returnNullOnMissingStub: true),
2727
MockSpec<Bar>(as: #MockBarRelaxed, returnNullOnMissingStub: true),
28-
MockSpec<Baz>(as: #MockBazWithUnsupportedMembers, unsupportedMembers: {
29-
#returnsTypeVariable,
30-
#returnsBoundedTypeVariable,
31-
#returnsTypeVariableFromTwo,
32-
#returnsGenericFunction,
33-
#typeVariableField,
34-
}),
35-
MockSpec<Baz>(as: #MockBazWithFallbackGenerators, fallbackGenerators: {
36-
#returnsTypeVariable: returnsTypeVariableShim,
37-
#returnsBoundedTypeVariable: returnsBoundedTypeVariableShim,
38-
#returnsTypeVariableFromTwo: returnsTypeVariableFromTwoShim,
39-
#returnsGenericFunction: returnsGenericFunctionShim,
40-
#typeVariableField: typeVariableFieldShim,
41-
}),
28+
MockSpec<Baz>(
29+
as: #MockBazWithUnsupportedMembers,
30+
unsupportedMembers: {
31+
#returnsTypeVariable,
32+
#returnsBoundedTypeVariable,
33+
#returnsTypeVariableFromTwo,
34+
#returnsGenericFunction,
35+
#typeVariableField,
36+
},
37+
),
38+
MockSpec<Baz>(
39+
as: #MockBazWithFallbackGenerators,
40+
fallbackGenerators: {
41+
#returnsTypeVariable: returnsTypeVariableShim,
42+
#returnsBoundedTypeVariable: returnsBoundedTypeVariableShim,
43+
#returnsTypeVariableFromTwo: returnsTypeVariableFromTwoShim,
44+
#returnsGenericFunction: returnsGenericFunctionShim,
45+
#typeVariableField: typeVariableFieldShim,
46+
},
47+
),
48+
MockSpec<HasPrivate>(mixingIn: [HasPrivateMixin]),
4249
])
4350
void main() {
4451
group('for a generated mock,', () {
@@ -257,4 +264,11 @@ void main() {
257264
when(foo.methodWithBarArg(bar)).thenReturn('mocked result');
258265
expect(foo.methodWithBarArg(bar), equals('mocked result'));
259266
});
267+
268+
test('a generated mock with a mixed in type can use mixed in members', () {
269+
var hasPrivate = MockHasPrivate();
270+
// This should not throw, when `setPrivate` accesses a private member on
271+
// `hasPrivate`.
272+
setPrivate(hasPrivate);
273+
});
260274
}

0 commit comments

Comments
 (0)