MockBuilder: Refactor exception-throwing paths; add more
* Almost all sanity checking was done in the _MockLibraryInfo constructor.
Yuck. Move this to a new method.
* Throw when a mock of the same name already exists. Someone is likely
migrating existing tests, and would be in for a surprise.
* Throw when asked to generate for two unique classes which have the same
name. The code-generating API does not yet have a mechanism for
user-supplied names to avoid such a conflict.
* Test that the GenerateMocks annotation can be written multiple times, and
that mocks from each occurrence are generated.
* Check that `entryLib` is not null. I can't reproduce this in a test, but
encountered it while testing on a real test: tableview_test.dart.
* Deduplicate classes which are asked to be mocked multiple times.
PiperOrigin-RevId: 315995820
diff --git a/lib/src/builder.dart b/lib/src/builder.dart
index b935030..db31c5a 100644
--- a/lib/src/builder.dart
+++ b/lib/src/builder.dart
@@ -39,9 +39,10 @@
@override
Future build(BuildStep buildStep) async {
final entryLib = await buildStep.inputLibrary;
+ if (entryLib == null) return;
final sourceLibIsNonNullable = entryLib.isNonNullableByDefault;
final mockLibraryAsset = buildStep.inputId.changeExtension('.mocks.dart');
- final classesToMock = <DartObject>[];
+ final objectsToMock = <DartObject>{};
for (final element in entryLib.topLevelElements) {
final annotation = element.metadata.firstWhere(
@@ -59,9 +60,14 @@
'The GenerateMocks "classes" argument is missing, includes an '
'unknown type, or includes an extension');
}
- classesToMock.addAll(classesField.toListValue());
+ objectsToMock.addAll(classesField.toListValue());
}
+ var classesToMock =
+ _mapAnnotationValuesToClasses(objectsToMock, entryLib.typeProvider);
+
+ _checkClassesToMockAreValid(classesToMock, entryLib);
+
final mockLibrary = Library((b) {
var mockLibraryInfo = _MockLibraryInfo(classesToMock,
sourceLibIsNonNullable: sourceLibIsNonNullable,
@@ -84,6 +90,99 @@
await buildStep.writeAsString(mockLibraryAsset, mockLibraryContent);
}
+ /// Map the values passed to the GenerateMocks annotation to the classes which
+ /// they represent.
+ ///
+ /// This function is responsible for ensuring that each value is an
+ /// appropriate target for mocking. It will throw an
+ /// [InvalidMockitoAnnotationException] under various conditions.
+ List<analyzer.DartType> _mapAnnotationValuesToClasses(
+ Iterable<DartObject> objectsToMock, TypeProvider typeProvider) {
+ var classesToMock = <analyzer.DartType>[];
+
+ for (final objectToMock in objectsToMock) {
+ final typeToMock = objectToMock.toTypeValue();
+ if (typeToMock == null) {
+ throw InvalidMockitoAnnotationException(
+ 'The "classes" argument includes a non-type: $objectToMock');
+ }
+
+ final elementToMock = typeToMock.element;
+ if (elementToMock is ClassElement) {
+ if (elementToMock.isEnum) {
+ throw InvalidMockitoAnnotationException(
+ 'The "classes" argument includes an enum: '
+ '${elementToMock.displayName}');
+ }
+ if (typeProvider.nonSubtypableClasses.contains(elementToMock)) {
+ throw InvalidMockitoAnnotationException(
+ 'The "classes" argument includes a non-subtypable type: '
+ '${elementToMock.displayName}. It is illegal to subtype this '
+ 'type.');
+ }
+ classesToMock.add(typeToMock);
+ } else if (elementToMock is GenericFunctionTypeElement &&
+ elementToMock.enclosingElement is FunctionTypeAliasElement) {
+ throw InvalidMockitoAnnotationException(
+ 'The "classes" argument includes a typedef: '
+ '${elementToMock.enclosingElement.displayName}');
+ } else {
+ throw InvalidMockitoAnnotationException(
+ 'The "classes" argument includes a non-class: '
+ '${elementToMock.displayName}');
+ }
+ }
+ return classesToMock;
+ }
+
+ void _checkClassesToMockAreValid(
+ List<analyzer.DartType> classesToMock, LibraryElement entryLib) {
+ var classesInEntryLib = entryLib.topLevelElements.whereType<ClassElement>();
+ var classNamesToMock = <String, ClassElement>{};
+ for (var class_ in classesToMock) {
+ var name = class_.element.name;
+ if (classNamesToMock.containsKey(name)) {
+ var firstSource = classNamesToMock[name].source.fullName;
+ var secondSource = class_.element.source.fullName;
+ // TODO(srawlins): Support an optional @GenerateMocks API that allows
+ // users to choose names. One class might be named MockFoo and the other
+ // named MockPbFoo, for example.
+ throw InvalidMockitoAnnotationException(
+ 'The GenerateMocks "classes" argument contains two classes with '
+ 'the same name: $name. One declared in $firstSource, the other in '
+ '$secondSource.');
+ }
+ classNamesToMock[name] = class_.element as ClassElement;
+ }
+ var classNamesToGenerate = classNamesToMock.keys.map((name) => 'Mock$name');
+ classNamesToMock.forEach((name, element) {
+ var conflictingClass = classesInEntryLib.firstWhere(
+ (c) => c.name == 'Mock${element.name}',
+ orElse: () => null);
+ if (conflictingClass != null) {
+ throw InvalidMockitoAnnotationException(
+ 'The GenerateMocks "classes" argument contains a class which '
+ 'conflicts with another class declared in this library: '
+ '${conflictingClass.name}');
+ }
+ var preexistingMock = classesInEntryLib.firstWhere(
+ (c) =>
+ c.interfaces.map((type) => type.element).contains(element) &&
+ _isMockClass(c.supertype),
+ orElse: () => null);
+ if (preexistingMock != null) {
+ throw InvalidMockitoAnnotationException(
+ 'The GenerateMocks "classes" argument contains a class which '
+ 'appears to already be mocked inline: ${preexistingMock.name}');
+ }
+ });
+ }
+
+ /// Return whether [type] is the Mock class declared by mockito.
+ bool _isMockClass(analyzer.InterfaceType type) =>
+ type.element.name == 'Mock' &&
+ type.element.source.fullName.endsWith('lib/src/mock.dart');
+
@override
final buildExtensions = const {
'.dart': ['.mocks.dart']
@@ -114,44 +213,16 @@
/// Build mock classes for [classesToMock], a list of classes obtained from a
/// `@GenerateMocks` annotation.
- _MockLibraryInfo(List<DartObject> classesToMock,
+ _MockLibraryInfo(List<analyzer.DartType> classesToMock,
{this.sourceLibIsNonNullable, this.typeProvider, this.typeSystem}) {
for (final classToMock in classesToMock) {
- final dartTypeToMock = classToMock.toTypeValue();
- if (dartTypeToMock == null) {
- throw InvalidMockitoAnnotationException(
- 'The "classes" argument includes a non-type: $classToMock');
- }
-
- final elementToMock = dartTypeToMock.element;
- if (elementToMock is ClassElement) {
- if (elementToMock.isEnum) {
- throw InvalidMockitoAnnotationException(
- 'The "classes" argument includes an enum: '
- '${elementToMock.displayName}');
- }
- if (typeProvider.nonSubtypableClasses.contains(elementToMock)) {
- throw InvalidMockitoAnnotationException(
- 'The "classes" argument includes a non-subtypable type: '
- '${elementToMock.displayName}. It is illegal to subtype this '
- 'type.');
- }
- mockClasses.add(_buildMockClass(dartTypeToMock, elementToMock));
- } else if (elementToMock is GenericFunctionTypeElement &&
- elementToMock.enclosingElement is FunctionTypeAliasElement) {
- throw InvalidMockitoAnnotationException(
- 'The "classes" argument includes a typedef: '
- '${elementToMock.enclosingElement.displayName}');
- } else {
- throw InvalidMockitoAnnotationException(
- 'The "classes" argument includes a non-class: '
- '${elementToMock.displayName}');
- }
+ mockClasses.add(_buildMockClass(classToMock));
}
}
- Class _buildMockClass(analyzer.DartType dartType, ClassElement classToMock) {
- final className = dartType.displayName;
+ Class _buildMockClass(analyzer.DartType dartType) {
+ final classToMock = dartType.element as ClassElement;
+ final className = dartType.name;
return Class((cBuilder) {
cBuilder
@@ -330,6 +401,9 @@
return _typeReference(dartType).property(
elementToFake.fields.firstWhere((f) => f.isEnumConstant).name);
} else {
+ // There is a potential for these names to collide. If one mock class
+ // requires a fake for a certain Foo, and another mock class requires a
+ // fake for a different Foo, they will collide.
var fakeName = '_Fake${dartType.name}';
// Only make one fake class for each class that needs to be faked.
if (!fakedClassElements.contains(elementToFake)) {
diff --git a/test/builder_test.dart b/test/builder_test.dart
index 409a23c..b703f40 100644
--- a/test/builder_test.dart
+++ b/test/builder_test.dart
@@ -33,6 +33,15 @@
'''
};
+const mockitoAssets = {
+ 'mockito|lib/mockito.dart': '''
+export 'src/mock.dart';
+''',
+ 'mockito|lib/src/mock.dart': '''
+class Mock {}
+'''
+};
+
const simpleTestAsset = {
'foo|test/foo_test.dart': '''
import 'package:foo/foo.dart';
@@ -176,6 +185,60 @@
);
});
+ test('deduplicates classes listed multiply in GenerateMocks', () async {
+ await _testWithNonNullable(
+ {
+ ...annotationsAsset,
+ 'foo|lib/foo.dart': dedent(r'''
+ class Foo {}
+ '''),
+ 'foo|test/foo_test.dart': '''
+ import 'package:foo/foo.dart';
+ import 'package:mockito/annotations.dart';
+ @GenerateMocks([Foo, Foo])
+ void main() {}
+ '''
+ },
+ outputs: {
+ 'foo|test/foo_test.mocks.dart': dedent(r'''
+ import 'package:mockito/mockito.dart' as _i1;
+ import 'package:foo/foo.dart' as _i2;
+
+ /// A class which mocks [Foo].
+ ///
+ /// See the documentation for Mockito's code generation for more information.
+ class MockFoo extends _i1.Mock implements _i2.Foo {}
+ '''),
+ },
+ );
+ });
+
+ test('generates mock classes from multiple annotations', () async {
+ await _testWithNonNullable(
+ {
+ ...annotationsAsset,
+ 'foo|lib/foo.dart': dedent(r'''
+ class Foo {}
+ class Bar {}
+ '''),
+ 'foo|test/foo_test.dart': '''
+ import 'package:foo/foo.dart';
+ import 'package:mockito/annotations.dart';
+ @GenerateMocks([Foo])
+ void fooTests() {}
+ @GenerateMocks([Bar])
+ void barTests() {}
+ '''
+ },
+ outputs: {
+ 'foo|test/foo_test.mocks.dart': _containsAllOf(
+ 'class MockFoo extends _i1.Mock implements _i2.Foo {}',
+ 'class MockBar extends _i1.Mock implements _i2.Bar {}',
+ ),
+ },
+ );
+ });
+
test('generates generic mock classes', () async {
await _expectSingleNonNullableOutput(
dedent(r'''
@@ -1040,6 +1103,74 @@
);
});
+ test('throws when two distinct classes with the same name are mocked',
+ () async {
+ _expectBuilderThrows(
+ assets: {
+ ...annotationsAsset,
+ 'foo|lib/a.dart': dedent(r'''
+ class Foo {}
+ '''),
+ 'foo|lib/b.dart': dedent(r'''
+ class Foo {}
+ '''),
+ 'foo|test/foo_test.dart': dedent('''
+ import 'package:foo/a.dart' as a;
+ import 'package:foo/b.dart' as b;
+ import 'package:mockito/annotations.dart';
+ @GenerateMocks([a.Foo, b.Foo])
+ void main() {}
+ '''),
+ },
+ message: contains(
+ 'contains two classes with the same name: Foo. One declared in '
+ '/foo/lib/a.dart, the other in /foo/lib/b.dart'),
+ );
+ });
+
+ test('throws when a mock class of the same name already exists', () async {
+ _expectBuilderThrows(
+ assets: {
+ ...annotationsAsset,
+ 'foo|lib/foo.dart': dedent(r'''
+ class Foo {}
+ '''),
+ 'foo|test/foo_test.dart': dedent('''
+ import 'package:foo/foo.dart';
+ import 'package:mockito/annotations.dart';
+ @GenerateMocks([Foo])
+ void main() {}
+ class MockFoo {}
+ '''),
+ },
+ message: contains(
+ 'contains a class which conflicts with another class declared in '
+ 'this library: MockFoo'),
+ );
+ });
+
+ test('throws when a mock class of class-to-mock already exists', () async {
+ _expectBuilderThrows(
+ assets: {
+ ...annotationsAsset,
+ ...mockitoAssets,
+ 'foo|lib/foo.dart': dedent(r'''
+ class Foo {}
+ '''),
+ 'foo|test/foo_test.dart': dedent('''
+ import 'package:foo/foo.dart';
+ import 'package:mockito/annotations.dart';
+ import 'package:mockito/mockito.dart';
+ @GenerateMocks([Foo])
+ void main() {}
+ class FakeFoo extends Mock implements Foo {}
+ '''),
+ },
+ message: contains(
+ 'contains a class which appears to already be mocked inline: FakeFoo'),
+ );
+ });
+
test('throws when GenerateMocks references a non-type', () async {
_expectBuilderThrows(
assets: {