MockBuilder: Add GenerateMocks customClasses field and constructor parameter.
This field allows more customizable mock generation:
* A mock class name can be specified, in order to avoid name collisions.
* A mock class can extend a class with type arguments. E.g.
@GenerateMocks([], customMocks: MockSpec<Foo<int>>())
generates
class MockFoo implements Mock extends Foo<int> {}
PiperOrigin-RevId: 321560783
diff --git a/lib/annotations.dart b/lib/annotations.dart
index bf1f5da..551778e 100644
--- a/lib/annotations.dart
+++ b/lib/annotations.dart
@@ -12,8 +12,62 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+/// An annotation to direct Mockito to generate mock classes.
+///
+/// During [code generation][NULL_SAFETY_README], Mockito will generate a
+/// `Mock{Type} extends Mock` class for each class to be mocked, in
+/// `{name}.mocks.dart`, where `{name}` is the basename of the file in which
+/// `@GenerateMocks` is used.
+///
+/// For example, if `@GenerateMocks([Foo])` is found at the top-level of a Dart
+/// library, `foo_test.dart`, then Mockito will generate
+/// `class MockFoo extends Mock implements Foo` in a new library,
+/// `foo_test.mocks.dart`.
+///
+/// If the class-to-mock is generic, then the mock will be identically generic.
+/// For example, given the class `class Foo<T, U>`, Mockito will generate
+/// `class MockFoo<T, U> extends Mock implements Foo<T, U>`.
+///
+/// Custom mocks can be generated with the `customMocks:` named argument. Each
+/// mock is specified with a [MockSpec] object.
+///
+/// [NULL_SAFETY_README]: https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md
class GenerateMocks {
final List<Type> classes;
+ final List<MockSpec> customMocks;
- const GenerateMocks(this.classes);
+ const GenerateMocks(this.classes, {this.customMocks = const []});
+}
+
+/// A specification of how to mock a specific class.
+///
+/// The type argument `T` is the class-to-mock. If this class is generic, and no
+/// explicit type arguments are given, then the mock class is generic.
+/// If the class is generic, and `T` has been specified with type argument(s),
+/// the mock class is not generic, and it extends the mocked class using the
+/// given type arguments.
+///
+/// The name of the mock class is either specified with the `as` named argument,
+/// or is the name of the class being mocked, prefixed with 'Mock'.
+///
+/// For example, given the generic class, `class Foo<T>`, then this
+/// annotation:
+///
+/// ```dart
+/// @GenerateMocks([], customMocks: [
+/// MockSpec<Foo>(),
+/// MockSpec<Foo<int>>(as: #MockFooOfInt),
+/// ])
+/// ```
+///
+/// directs Mockito to generate two mocks:
+/// `class MockFoo<T> extends Mocks implements Foo<T>` and
+/// `class MockFooOfInt extends Mock implements Foo<int>`.
+// TODO(srawlins): Document this in NULL_SAFETY_README.md.
+// TODO(srawlins): Add 'returnNullOnMissingStub'.
+// TODO(srawlins): Add 'mixingIn'.
+class MockSpec<T> {
+ final Symbol mockName;
+
+ const MockSpec({Symbol as}) : mockName = as;
}
diff --git a/lib/src/builder.dart b/lib/src/builder.dart
index d8aaacb..fe21e9f 100644
--- a/lib/src/builder.dart
+++ b/lib/src/builder.dart
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart' as analyzer;
import 'package:analyzer/dart/element/type_provider.dart';
@@ -46,7 +45,7 @@
final mockTargetGatherer = _MockTargetGatherer(entryLib);
final mockLibrary = Library((b) {
- var mockLibraryInfo = _MockLibraryInfo(mockTargetGatherer._classesToMock,
+ var mockLibraryInfo = _MockLibraryInfo(mockTargetGatherer._mockTargets,
sourceLibIsNonNullable: sourceLibIsNonNullable,
typeProvider: entryLib.typeProvider,
typeSystem: entryLib.typeSystem);
@@ -73,16 +72,26 @@
};
}
+class _MockTarget {
+ /// The class to be mocked.
+ final analyzer.InterfaceType classType;
+
+ /// The desired name of the mock class.
+ final String mockName;
+
+ _MockTarget(this.classType, this.mockName);
+
+ ClassElement get classElement => classType.element;
+}
+
/// This class gathers and verifies mock targets referenced in `GenerateMocks`
/// annotations.
-// TODO(srawlins): This also needs to gather mock targets (with overridden
-// names, type arguments, etc.) found in `GenerateMock` annotations.
class _MockTargetGatherer {
final LibraryElement _entryLib;
- final List<analyzer.DartType> _classesToMock;
+ final List<_MockTarget> _mockTargets;
- _MockTargetGatherer._(this._entryLib, this._classesToMock) {
+ _MockTargetGatherer._(this._entryLib, this._mockTargets) {
_checkClassesToMockAreValid();
}
@@ -90,34 +99,68 @@
/// annotations and creates a [_MockTargetGatherer] with all of the classes
/// identified as mocking targets.
factory _MockTargetGatherer(LibraryElement entryLib) {
- final objectsToMock = <DartObject>{};
+ final mockTargets = <_MockTarget>{};
for (final element in entryLib.topLevelElements) {
// TODO(srawlins): Re-think the idea of multiple @GenerateMocks
// annotations, on one element or even on different elements in a library.
for (final annotation in element.metadata) {
if (annotation == null) continue;
- if (annotation.element is! ConstructorElement ||
- annotation.element.enclosingElement.name != 'GenerateMocks') {
- continue;
+ if (annotation.element is! ConstructorElement) continue;
+ final annotationClass = annotation.element.enclosingElement.name;
+ // TODO(srawlins): check library as well.
+ if (annotationClass == 'GenerateMocks') {
+ mockTargets
+ .addAll(_mockTargetsFromGenerateMocks(annotation, entryLib));
}
- final generateMocksValue = annotation.computeConstantValue();
- // TODO(srawlins): handle `generateMocksValue == null`?
- // I am unable to think of a case which results in this situation.
- final classesField = generateMocksValue.getField('classes');
- if (classesField.isNull) {
- throw InvalidMockitoAnnotationException(
- 'The GenerateMocks "classes" argument is missing, includes an '
- 'unknown type, or includes an extension');
- }
- objectsToMock.addAll(classesField.toListValue());
}
}
- var classesToMock =
- _mapAnnotationValuesToClasses(objectsToMock, entryLib.typeProvider);
+ return _MockTargetGatherer._(entryLib, mockTargets.toList());
+ }
- return _MockTargetGatherer._(entryLib, classesToMock);
+ static Iterable<_MockTarget> _mockTargetsFromGenerateMocks(
+ ElementAnnotation annotation, LibraryElement entryLib) {
+ final generateMocksValue = annotation.computeConstantValue();
+ final classesField = generateMocksValue.getField('classes');
+ if (classesField.isNull) {
+ throw InvalidMockitoAnnotationException(
+ 'The GenerateMocks "classes" argument is missing, includes an '
+ 'unknown type, or includes an extension');
+ }
+ final mockTargets = <_MockTarget>[];
+ for (var objectToMock in classesField.toListValue()) {
+ final typeToMock = objectToMock.toTypeValue();
+ if (typeToMock == null) {
+ throw InvalidMockitoAnnotationException(
+ 'The "classes" argument includes a non-type: $objectToMock');
+ }
+ if (typeToMock.isDynamic) {
+ throw InvalidMockitoAnnotationException(
+ 'Mockito cannot mock `dynamic`');
+ }
+ final type = _determineDartType(typeToMock, entryLib.typeProvider);
+ final mockName = 'Mock${type.element.name}';
+ mockTargets.add(_MockTarget(type, mockName));
+ }
+ final customMocksField = generateMocksValue.getField('customMocks');
+ if (customMocksField != null && !customMocksField.isNull) {
+ for (var mockSpec in customMocksField.toListValue()) {
+ final mockSpecType = mockSpec.type;
+ assert(mockSpecType.typeArguments.length == 1);
+ final typeToMock = mockSpecType.typeArguments.single;
+ if (typeToMock.isDynamic) {
+ throw InvalidMockitoAnnotationException(
+ 'Mockito cannot mock `dynamic`; be sure to declare type '
+ 'arguments on MockSpec(), in @GenerateMocks.');
+ }
+ var type = _determineDartType(typeToMock, entryLib.typeProvider);
+ final mockName = mockSpec.getField('mockName').toSymbolValue() ??
+ 'Mock${type.element.name}';
+ mockTargets.add(_MockTarget(type, mockName));
+ }
+ }
+ return mockTargets;
}
/// Map the values passed to the GenerateMocks annotation to the classes which
@@ -126,89 +169,75 @@
/// This function is responsible for ensuring that each value is an
/// appropriate target for mocking. It will throw an
/// [InvalidMockitoAnnotationException] under various conditions.
- static 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) {
+ static analyzer.InterfaceType _determineDartType(
+ analyzer.DartType typeToMock, TypeProvider typeProvider) {
+ final elementToMock = typeToMock.element;
+ if (elementToMock is ClassElement) {
+ if (elementToMock.isEnum) {
throw InvalidMockitoAnnotationException(
- 'The "classes" argument includes a non-type: $objectToMock');
+ 'Mockito cannot mock an enum: ${elementToMock.displayName}');
}
-
- 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.');
- }
- if (elementToMock.isPrivate) {
- throw InvalidMockitoAnnotationException(
- 'The "classes" argument includes a private type: '
- '${elementToMock.displayName}.');
- }
- var typeParameterErrors =
- _checkTypeParameters(elementToMock.typeParameters, elementToMock);
- if (typeParameterErrors.isNotEmpty) {
- var joinedMessages =
- typeParameterErrors.map((m) => ' $m').join('\n');
- throw InvalidMockitoAnnotationException(
- 'Mockito cannot generate a valid mock class which implements '
- "'${elementToMock.displayName}' for the following reasons:\n"
- '$joinedMessages');
- }
- classesToMock.add(typeToMock);
- } else if (elementToMock is GenericFunctionTypeElement &&
- elementToMock.enclosingElement is FunctionTypeAliasElement) {
+ if (typeProvider.nonSubtypableClasses.contains(elementToMock)) {
throw InvalidMockitoAnnotationException(
- 'The "classes" argument includes a typedef: '
- '${elementToMock.enclosingElement.displayName}');
- } else {
- throw InvalidMockitoAnnotationException(
- 'The "classes" argument includes a non-class: '
- '${elementToMock.displayName}');
+ 'Mockito cannot mock a non-subtypable type: '
+ '${elementToMock.displayName}. It is illegal to subtype this '
+ 'type.');
}
+ if (elementToMock.isPrivate) {
+ throw InvalidMockitoAnnotationException(
+ 'Mockito cannot mock a private type: '
+ '${elementToMock.displayName}.');
+ }
+ var typeParameterErrors =
+ _checkTypeParameters(elementToMock.typeParameters, elementToMock);
+ if (typeParameterErrors.isNotEmpty) {
+ var joinedMessages =
+ typeParameterErrors.map((m) => ' $m').join('\n');
+ throw InvalidMockitoAnnotationException(
+ 'Mockito cannot generate a valid mock class which implements '
+ "'${elementToMock.displayName}' for the following reasons:\n"
+ '$joinedMessages');
+ }
+ return typeToMock as analyzer.InterfaceType;
+ } else if (elementToMock is GenericFunctionTypeElement &&
+ elementToMock.enclosingElement is FunctionTypeAliasElement) {
+ throw InvalidMockitoAnnotationException('Mockito cannot mock a typedef: '
+ '${elementToMock.enclosingElement.displayName}');
+ } else {
+ throw InvalidMockitoAnnotationException(
+ 'Mockito cannot mock a non-class: ${elementToMock.displayName}');
}
- return classesToMock;
}
void _checkClassesToMockAreValid() {
var classesInEntryLib =
_entryLib.topLevelElements.whereType<ClassElement>();
var classNamesToMock = <String, ClassElement>{};
- for (var class_ in _classesToMock) {
- var name = class_.element.name;
+ var uniqueNameSuggestion =
+ "use the 'customMocks' argument in @GenerateMocks to specify a unique "
+ 'name';
+ for (final mockTarget in _mockTargets) {
+ var name = mockTarget.mockName;
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.
+ var secondSource = mockTarget.classElement.source.fullName;
throw InvalidMockitoAnnotationException(
- 'The GenerateMocks "classes" argument contains two classes with '
- 'the same name: $name. One declared in $firstSource, the other in '
- '$secondSource.');
+ 'Mockito cannot generate two mocks with the same name: $name (for '
+ '${classNamesToMock[name].name} declared in $firstSource, and for '
+ '${mockTarget.classElement.name} declared in $secondSource); '
+ '$uniqueNameSuggestion.');
}
- classNamesToMock[name] = class_.element as ClassElement;
+ classNamesToMock[name] = mockTarget.classElement;
}
classNamesToMock.forEach((name, element) {
- var conflictingClass = classesInEntryLib.firstWhere(
- (c) => c.name == 'Mock${element.name}',
+ var conflictingClass = classesInEntryLib.firstWhere((c) => c.name == 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}');
+ 'Mockito cannot generate a mock with a name which conflicts with '
+ 'another class declared in this library: ${conflictingClass.name}; '
+ '$uniqueNameSuggestion.');
}
var preexistingMock = classesInEntryLib.firstWhere(
@@ -218,8 +247,9 @@
orElse: () => null);
if (preexistingMock != null) {
throw InvalidMockitoAnnotationException(
- 'The GenerateMocks "classes" argument contains a class which '
- 'appears to already be mocked inline: ${preexistingMock.name}');
+ 'The GenerateMocks annotation contains a class which appears to '
+ 'already be mocked inline: ${preexistingMock.name}; '
+ '$uniqueNameSuggestion.');
}
_checkMethodsToStubAreValid(element);
@@ -376,23 +406,48 @@
/// fake classes are added to the generated library.
final fakedClassElements = <ClassElement>[];
- /// Build mock classes for [classesToMock], a list of classes obtained from a
- /// `@GenerateMocks` annotation.
- _MockLibraryInfo(List<analyzer.DartType> classesToMock,
+ /// Build mock classes for [mockTargets].
+ _MockLibraryInfo(Iterable<_MockTarget> mockTargets,
{this.sourceLibIsNonNullable, this.typeProvider, this.typeSystem}) {
- for (final classToMock in classesToMock) {
- mockClasses.add(_buildMockClass(classToMock));
+ for (final mockTarget in mockTargets) {
+ mockClasses.add(_buildMockClass(mockTarget));
}
}
- Class _buildMockClass(analyzer.DartType dartType) {
- final classToMock = dartType.element as ClassElement;
- final className = dartType.name;
- final mockClassName = 'Mock$className';
+ bool _hasExplicitTypeArguments(analyzer.InterfaceType type) {
+ if (type.typeArguments == null) return false;
+
+ // If it appears that one type argument was given, then they all were. This
+ // returns the wrong result when the type arguments given are all `dynamic`,
+ // or are each equal to the bound of the corresponding type parameter. There
+ // may not be a way to get around this.
+ for (var i = 0; i < type.typeArguments.length; i++) {
+ var typeArgument = type.typeArguments[i];
+ var bound =
+ type.element.typeParameters[i].bound ?? typeProvider.dynamicType;
+ // If [type] was given to @GenerateMocks as a Type, and no explicit type
+ // argument is given, [typeArgument] is `dynamic` (_not_ the bound, as one
+ // might think). We determine that an explicit type argument was given if
+ // it is not `dynamic`.
+ //
+ // If, on the other hand, [type] was given to @GenerateMock as a type
+ // argument to `Of()`, and no type argument is given, [typeArgument] is
+ // the bound of the corresponding type paramter (dynamic or otherwise). We
+ // determine that an explicit type argument was given if [typeArgument] is
+ // is not [bound].
+ if (!typeArgument.isDynamic && typeArgument != bound) return true;
+ }
+ return false;
+ }
+
+ Class _buildMockClass(_MockTarget mockTarget) {
+ final typeToMock = mockTarget.classType;
+ final classToMock = mockTarget.classElement;
+ final className = classToMock.name;
return Class((cBuilder) {
cBuilder
- ..name = mockClassName
+ ..name = mockTarget.mockName
..extend = refer('Mock', 'package:mockito/mockito.dart')
..docs.add('/// A class which mocks [$className].')
..docs.add('///')
@@ -402,7 +457,19 @@
// parameter with same type variables, and a mirrored type argument for
// the "implements" clause.
var typeArguments = <Reference>[];
- if (classToMock.typeParameters != null) {
+ if (_hasExplicitTypeArguments(typeToMock)) {
+ // [typeToMock] is a reference to a type with type arguments (for
+ // example: `Foo<int>`). Generate a non-generic mock class which
+ // implements the mock target with said type arguments. For example:
+ // `class MockFoo extends Mock implements Foo<int> {}`
+ for (var typeArgument in typeToMock.typeArguments) {
+ typeArguments.add(refer(typeArgument.element.name));
+ }
+ } else if (classToMock.typeParameters != null) {
+ // [typeToMock] is a simple reference to a generic type (for example:
+ // `Foo`, a reference to `class Foo<T> {}`). Generate a generic mock
+ // class which perfectly mirrors the type parameters on [typeToMock],
+ // forwarding them to the "implements" clause.
for (var typeParameter in classToMock.typeParameters) {
cBuilder.types.add(_typeParameterReference(typeParameter));
typeArguments.add(refer(typeParameter.name));
@@ -410,8 +477,8 @@
}
cBuilder.implements.add(TypeReference((b) {
b
- ..symbol = dartType.name
- ..url = _typeImport(dartType)
+ ..symbol = classToMock.name
+ ..url = _typeImport(mockTarget.classType)
..types.addAll(typeArguments);
}));
diff --git a/test/builder_test.dart b/test/builder/auto_mocks_test.dart
similarity index 97%
rename from test/builder_test.dart
rename to test/builder/auto_mocks_test.dart
index 8ca269b..c456abe 100644
--- a/test/builder_test.dart
+++ b/test/builder/auto_mocks_test.dart
@@ -27,8 +27,15 @@
'mockito|lib/annotations.dart': '''
class GenerateMocks {
final List<Type> classes;
+ final List<MockSpec> customMocks;
- const GenerateMocks(this.classes);
+ const GenerateMocks(this.classes, {this.customMocks = []});
+}
+
+class MockSpec<T> {
+ final Symbol mockName;
+
+ const MockSpec({Symbol as}) : mockName = as;
}
'''
};
@@ -185,34 +192,6 @@
);
});
- 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(
{
@@ -1194,6 +1173,27 @@
);
});
+ test('throws when GenerateMocks is given a class multiple times', () async {
+ _expectBuilderThrows(
+ assets: {
+ ...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() {}
+ '''
+ },
+ message: contains(
+ 'Mockito cannot generate two mocks with the same name: MockFoo (for '
+ 'Foo declared in /foo/lib/foo.dart, and for Foo declared in '
+ '/foo/lib/foo.dart)'),
+ );
+ });
+
test(
'throws when GenerateMocks is given a class with a method with a '
'private return type', () async {
@@ -1452,14 +1452,12 @@
...annotationsAsset,
'foo|test/foo_test.dart': dedent('''
import 'package:mockito/annotations.dart';
- // Missing required argument to GenerateMocks.
@GenerateMocks([_Foo])
void main() {}
class _Foo {}
'''),
},
- message:
- contains('The "classes" argument includes a private type: _Foo.'),
+ message: contains('Mockito cannot mock a private type: _Foo.'),
);
});
@@ -1501,8 +1499,9 @@
'''),
},
message: contains(
- 'contains two classes with the same name: Foo. One declared in '
- '/foo/lib/a.dart, the other in /foo/lib/b.dart'),
+ 'Mockito cannot generate two mocks with the same name: MockFoo (for '
+ 'Foo declared in /foo/lib/a.dart, and for Foo declared in '
+ '/foo/lib/b.dart)'),
);
});
@@ -1522,8 +1521,8 @@
'''),
},
message: contains(
- 'contains a class which conflicts with another class declared in '
- 'this library: MockFoo'),
+ 'Mockito cannot generate a mock with a name which conflicts with '
+ 'another class declared in this library: MockFoo'),
);
});
@@ -1572,7 +1571,7 @@
typedef Foo = void Function();
'''),
},
- message: 'The "classes" argument includes a typedef: Foo',
+ message: 'Mockito cannot mock a typedef: Foo',
);
});
@@ -1585,7 +1584,7 @@
enum Foo {}
'''),
},
- message: 'The "classes" argument includes an enum: Foo',
+ message: 'Mockito cannot mock an enum: Foo',
);
});
@@ -1612,8 +1611,7 @@
void main() {}
'''),
},
- message: contains(
- 'The "classes" argument includes a non-subtypable type: int'),
+ message: contains('Mockito cannot mock a non-subtypable type: int'),
);
});
diff --git a/test/builder/custom_mocks_test.dart b/test/builder/custom_mocks_test.dart
new file mode 100644
index 0000000..fbcfa1f
--- /dev/null
+++ b/test/builder/custom_mocks_test.dart
@@ -0,0 +1,474 @@
+// Copyright 2020 Dart Mockito authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@TestOn('vm')
+import 'package:build/build.dart';
+import 'package:build/experiments.dart';
+import 'package:build_test/build_test.dart';
+import 'package:meta/meta.dart';
+import 'package:mockito/src/builder.dart';
+import 'package:package_config/package_config.dart';
+import 'package:test/test.dart';
+
+Builder buildMocks(BuilderOptions options) => MockBuilder();
+
+const annotationsAsset = {
+ 'mockito|lib/annotations.dart': '''
+class GenerateMocks {
+ final List<Type> classes;
+ final List<MockSpec> customMocks;
+
+ const GenerateMocks(this.classes, {this.customMocks = []});
+}
+
+class MockSpec<T> {
+ final Symbol mockName;
+
+ const MockSpec({Symbol as}) : mockName = as;
+}
+'''
+};
+
+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';
+import 'package:mockito/annotations.dart';
+@GenerateMocks([], customMocks: [MockSpec<Foo>()])
+void main() {}
+'''
+};
+
+void main() {
+ test('generates a generic mock class without type arguments', () async {
+ await _testWithNonNullable(
+ {
+ ...annotationsAsset,
+ 'foo|lib/foo.dart': dedent(r'''
+ class Foo<T> {}
+ '''),
+ 'foo|test/foo_test.dart': '''
+ import 'package:foo/foo.dart';
+ import 'package:mockito/annotations.dart';
+ @GenerateMocks([], customMocks: [MockSpec<Foo>(as: #MockFoo)])
+ void main() {}
+ '''
+ },
+ outputs: {
+ 'foo|test/foo_test.mocks.dart': _containsAllOf(
+ 'class MockFoo<T> extends _i1.Mock implements _i2.Foo<T> {}',
+ ),
+ },
+ );
+ });
+
+ test('generates a generic mock class with type arguments', () async {
+ await _testWithNonNullable(
+ {
+ ...annotationsAsset,
+ 'foo|lib/foo.dart': dedent(r'''
+ class Foo<T, U> {}
+ '''),
+ 'foo|test/foo_test.dart': '''
+ import 'package:foo/foo.dart';
+ import 'package:mockito/annotations.dart';
+ @GenerateMocks(
+ [], customMocks: [MockSpec<Foo<int, bool>>(as: #MockFooOfIntBool)])
+ void main() {}
+ '''
+ },
+ outputs: {
+ 'foo|test/foo_test.mocks.dart': _containsAllOf(
+ 'class MockFooOfIntBool extends _i1.Mock implements _i2.Foo<int, bool> {}',
+ ),
+ },
+ );
+ });
+
+ test('generates a generic mock class with type arguments but no name',
+ () async {
+ await _testWithNonNullable(
+ {
+ ...annotationsAsset,
+ 'foo|lib/foo.dart': dedent(r'''
+ class Foo<T> {}
+ '''),
+ 'foo|test/foo_test.dart': '''
+ import 'package:foo/foo.dart';
+ import 'package:mockito/annotations.dart';
+ @GenerateMocks([], customMocks: [MockSpec<Foo<int>>()])
+ void main() {}
+ '''
+ },
+ outputs: {
+ 'foo|test/foo_test.mocks.dart': _containsAllOf(
+ 'class MockFoo extends _i1.Mock implements _i2.Foo<int> {}',
+ ),
+ },
+ );
+ });
+
+ test('generates a generic, bounded mock class without type arguments',
+ () async {
+ await _testWithNonNullable(
+ {
+ ...annotationsAsset,
+ 'foo|lib/foo.dart': dedent(r'''
+ class Foo<T extends Object> {}
+ '''),
+ 'foo|test/foo_test.dart': '''
+ import 'package:foo/foo.dart';
+ import 'package:mockito/annotations.dart';
+ @GenerateMocks([], customMocks: [MockSpec<Foo>(as: #MockFoo)])
+ void main() {}
+ '''
+ },
+ outputs: {
+ 'foo|test/foo_test.mocks.dart': _containsAllOf(
+ 'class MockFoo<T extends Object> extends _i1.Mock implements _i2.Foo<T> {}',
+ ),
+ },
+ );
+ });
+
+ 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([], customMocks: [MockSpec<Foo>()])
+ void fooTests() {}
+ @GenerateMocks([], customMocks: [MockSpec<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 mock classes from multiple annotations on a single element',
+ () async {
+ await _testWithNonNullable(
+ {
+ ...annotationsAsset,
+ 'foo|lib/a.dart': dedent(r'''
+ class Foo {}
+ '''),
+ 'foo|lib/b.dart': dedent(r'''
+ class Foo {}
+ '''),
+ 'foo|test/foo_test.dart': '''
+ import 'package:foo/a.dart' as a;
+ import 'package:foo/b.dart' as b;
+ import 'package:mockito/annotations.dart';
+ @GenerateMocks([], customMocks: [MockSpec<a.Foo>(as: #MockAFoo)])
+ @GenerateMocks([], customMocks: [MockSpec<b.Foo>(as: #MockBFoo)])
+ void main() {}
+ '''
+ },
+ outputs: {
+ 'foo|test/foo_test.mocks.dart': _containsAllOf(
+ 'class MockAFoo extends _i1.Mock implements _i2.Foo {}',
+ 'class MockBFoo extends _i1.Mock implements _i3.Foo {}',
+ ),
+ },
+ );
+ });
+
+ test(
+ 'throws when GenerateMock is given a class with a type parameter with a '
+ 'private bound', () async {
+ _expectBuilderThrows(
+ assets: {
+ ...annotationsAsset,
+ 'foo|lib/foo.dart': dedent(r'''
+ class Foo<T extends _Bar> {
+ void m(int a) {}
+ }
+ class _Bar {}
+ '''),
+ 'foo|test/foo_test.dart': dedent('''
+ import 'package:foo/foo.dart';
+ import 'package:mockito/annotations.dart';
+ @GenerateMocks([], customMocks: [MockSpec<Foo>()])
+ void main() {}
+ '''),
+ },
+ message: contains(
+ "The class 'Foo' features a private type parameter bound, and cannot "
+ 'be stubbed.'),
+ );
+ });
+
+ test("throws when GenerateMock's Of argument is missing a type argument",
+ () async {
+ _expectBuilderThrows(
+ assets: {
+ ...annotationsAsset,
+ 'foo|test/foo_test.dart': dedent('''
+ import 'package:mockito/annotations.dart';
+ // Missing required type argument to GenerateMock.
+ @GenerateMocks([], customMocks: [MockSpec()])
+ void main() {}
+ '''),
+ },
+ message: contains('Mockito cannot mock `dynamic`'),
+ );
+ });
+
+ test('throws when GenerateMock is given a private class', () async {
+ _expectBuilderThrows(
+ assets: {
+ ...annotationsAsset,
+ 'foo|test/foo_test.dart': dedent('''
+ import 'package:mockito/annotations.dart';
+ @GenerateMocks([], customMocks: [MockSpec<_Foo>()])
+ void main() {}
+ class _Foo {}
+ '''),
+ },
+ message: contains('Mockito cannot mock a private type: _Foo.'),
+ );
+ });
+
+ 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([], customMocks: [MockSpec<a.Foo>()])
+ @GenerateMocks([], customMocks: [MockSpec<b.Foo>()])
+ void main() {}
+ '''),
+ },
+ message: contains(
+ 'Mockito cannot generate two mocks with the same name: MockFoo (for '
+ 'Foo declared in /foo/lib/a.dart, and for Foo declared 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([], customMocks: [MockSpec<Foo>()])
+ void main() {}
+ class MockFoo {}
+ '''),
+ },
+ message: contains(
+ 'Mockito cannot generate a mock with a name 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([], customMocks: [MockSpec<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 GenerateMock references a typedef', () async {
+ _expectBuilderThrows(
+ assets: {
+ ...annotationsAsset,
+ ...simpleTestAsset,
+ 'foo|lib/foo.dart': dedent(r'''
+ typedef Foo = void Function();
+ '''),
+ },
+ message: 'Mockito cannot mock a typedef: Foo',
+ );
+ });
+
+ test('throws when GenerateMock references an enum', () async {
+ _expectBuilderThrows(
+ assets: {
+ ...annotationsAsset,
+ ...simpleTestAsset,
+ 'foo|lib/foo.dart': dedent(r'''
+ enum Foo {}
+ '''),
+ },
+ message: 'Mockito cannot mock an enum: Foo',
+ );
+ });
+
+ test('throws when GenerateMock references a non-subtypeable type', () async {
+ _expectBuilderThrows(
+ assets: {
+ ...annotationsAsset,
+ 'foo|test/foo_test.dart': dedent('''
+ import 'package:mockito/annotations.dart';
+ @GenerateMocks([], customMocks: [MockSpec<int>()])
+ void main() {}
+ '''),
+ },
+ message: contains('Mockito cannot mock a non-subtypable type: int'),
+ );
+ });
+
+ test('given a pre-non-nullable library, does not override any members',
+ () async {
+ await _testPreNonNullable(
+ {
+ ...annotationsAsset,
+ ...simpleTestAsset,
+ 'foo|lib/foo.dart': dedent(r'''
+ abstract class Foo {
+ int f(int a);
+ }
+ '''),
+ },
+ outputs: {
+ 'foo|test/foo_test.mocks.dart': _containsAllOf(
+ 'class MockFoo extends _i1.Mock implements _i2.Foo {}'),
+ },
+ );
+ });
+}
+
+/// Test [MockBuilder] in a package which has not opted into the non-nullable
+/// type system.
+///
+/// Whether the non-nullable experiment is enabled depends on the SDK executing
+/// this test, but that does not affect the opt-in state of the package under
+/// test.
+Future<void> _testPreNonNullable(Map<String, String> sourceAssets,
+ {Map<String, /*String|Matcher<String>*/ dynamic> outputs}) async {
+ var packageConfig = PackageConfig([
+ Package('foo', Uri.file('/foo/'),
+ packageUriRoot: Uri.file('/foo/lib/'),
+ languageVersion: LanguageVersion(2, 7))
+ ]);
+ await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
+ outputs: outputs, packageConfig: packageConfig);
+}
+
+/// Test [MockBuilder] in a package which has opted into the non-nullable type
+/// system, and with the non-nullable experiment enabled.
+Future<void> _testWithNonNullable(Map<String, String> sourceAssets,
+ {Map<String, /*String|Matcher<List<int>>*/ dynamic> outputs}) async {
+ var packageConfig = PackageConfig([
+ Package('foo', Uri.file('/foo/'),
+ packageUriRoot: Uri.file('/foo/lib/'),
+ languageVersion: LanguageVersion(2, 9))
+ ]);
+ await withEnabledExperiments(
+ () async => await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
+ outputs: outputs, packageConfig: packageConfig),
+ ['non-nullable'],
+ );
+}
+
+/// Test [MockBuilder] on a single source file, in a package which has opted
+/// into the non-nullable type system, and with the non-nullable experiment
+/// enabled.
+Future<void> _expectSingleNonNullableOutput(
+ String sourceAssetText,
+ /*String|Matcher<List<int>>*/ dynamic output) async {
+ var packageConfig = PackageConfig([
+ Package('foo', Uri.file('/foo/'),
+ packageUriRoot: Uri.file('/foo/lib/'),
+ languageVersion: LanguageVersion(2, 9))
+ ]);
+
+ await withEnabledExperiments(
+ () async => await testBuilder(
+ buildMocks(BuilderOptions({})),
+ {
+ ...annotationsAsset,
+ ...simpleTestAsset,
+ 'foo|lib/foo.dart': sourceAssetText,
+ },
+ outputs: {'foo|test/foo_test.mocks.dart': output},
+ packageConfig: packageConfig),
+ ['non-nullable'],
+ );
+}
+
+TypeMatcher<List<int>> _containsAllOf(a, [b]) => decodedMatches(
+ b == null ? allOf(contains(a)) : allOf(contains(a), contains(b)));
+
+/// Expect that [testBuilder], given [assets], throws an
+/// [InvalidMockitoAnnotationException] with a message containing [message].
+void _expectBuilderThrows(
+ {@required Map<String, String> assets,
+ @required dynamic /*String|Matcher<List<int>>*/ message}) {
+ expect(
+ () async => await testBuilder(buildMocks(BuilderOptions({})), assets),
+ throwsA(TypeMatcher<InvalidMockitoAnnotationException>()
+ .having((e) => e.message, 'message', message)));
+}
+
+/// Dedent [input], so that each line is shifted to the left, so that the first
+/// line is at the 0 column.
+String dedent(String input) {
+ final indentMatch = RegExp(r'^(\s*)').firstMatch(input);
+ final indent = ''.padRight(indentMatch.group(1).length);
+ return input.splitMapJoin('\n',
+ onNonMatch: (s) => s.replaceFirst(RegExp('^$indent'), ''));
+}