MockBuilder: Change default generated mocks to throw on missing stubs.
This also adds a mechanism to MockSpec (used in custom mocks) to revert to the
legacy behavior (returning null on missing stubs).
PiperOrigin-RevId: 322261918
diff --git a/lib/annotations.dart b/lib/annotations.dart
index 551778e..36540c6 100644
--- a/lib/annotations.dart
+++ b/lib/annotations.dart
@@ -50,6 +50,9 @@
/// 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'.
///
+/// To use the legacy behavior of returning null for unstubbed methods, use
+/// `returnNullOnMissingStub: true`.
+///
/// For example, given the generic class, `class Foo<T>`, then this
/// annotation:
///
@@ -64,10 +67,12 @@
/// `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;
+ final bool returnNullOnMissingStub;
+
+ const MockSpec({Symbol as, this.returnNullOnMissingStub = false})
+ : mockName = as;
}
diff --git a/lib/src/builder.dart b/lib/src/builder.dart
index fe21e9f..cb3ff7d 100644
--- a/lib/src/builder.dart
+++ b/lib/src/builder.dart
@@ -79,7 +79,9 @@
/// The desired name of the mock class.
final String mockName;
- _MockTarget(this.classType, this.mockName);
+ final bool returnNullOnMissingStub;
+
+ _MockTarget(this.classType, this.mockName, {this.returnNullOnMissingStub});
ClassElement get classElement => classType.element;
}
@@ -141,7 +143,8 @@
}
final type = _determineDartType(typeToMock, entryLib.typeProvider);
final mockName = 'Mock${type.element.name}';
- mockTargets.add(_MockTarget(type, mockName));
+ mockTargets
+ .add(_MockTarget(type, mockName, returnNullOnMissingStub: false));
}
final customMocksField = generateMocksValue.getField('customMocks');
if (customMocksField != null && !customMocksField.isNull) {
@@ -157,7 +160,10 @@
var type = _determineDartType(typeToMock, entryLib.typeProvider);
final mockName = mockSpec.getField('mockName').toSymbolValue() ??
'Mock${type.element.name}';
- mockTargets.add(_MockTarget(type, mockName));
+ final returnNullOnMissingStub =
+ mockSpec.getField('returnNullOnMissingStub').toBoolValue();
+ mockTargets.add(_MockTarget(type, mockName,
+ returnNullOnMissingStub: returnNullOnMissingStub));
}
}
return mockTargets;
@@ -481,6 +487,9 @@
..url = _typeImport(mockTarget.classType)
..types.addAll(typeArguments);
}));
+ if (!mockTarget.returnNullOnMissingStub) {
+ cBuilder.constructors.add(_constructorWithThrowOnMissingStub);
+ }
// Only override members of a class declared in a library which uses the
// non-nullable type system.
@@ -515,6 +524,14 @@
});
}
+ /// The default behavior of mocks is to return null for unstubbed methods. To
+ /// use the new behavior of throwing an error, we must explicitly call
+ /// `throwOnMissingStub`.
+ Constructor get _constructorWithThrowOnMissingStub =>
+ Constructor((cBuilder) => cBuilder.body =
+ refer('throwOnMissingStub', 'package:mockito/mockito.dart')
+ .call([refer('this').expression]).statement);
+
bool _returnTypeIsNonNullable(ExecutableElement method) =>
typeSystem.isPotentiallyNonNullable(method.returnType);
diff --git a/test/builder/auto_mocks_test.dart b/test/builder/auto_mocks_test.dart
index c456abe..1ebeb63 100644
--- a/test/builder/auto_mocks_test.dart
+++ b/test/builder/auto_mocks_test.dart
@@ -35,7 +35,10 @@
class MockSpec<T> {
final Symbol mockName;
- const MockSpec({Symbol as}) : mockName = as;
+ final bool returnNullOnMissingStub;
+
+ const MockSpec({Symbol as, this.returnNullOnMissingStub = false})
+ : mockName = as;
}
'''
};
@@ -58,6 +61,11 @@
'''
};
+const _constructorWithThrowOnMissingStub = '''
+MockFoo() {
+ _i1.throwOnMissingStub(this);
+ }''';
+
void main() {
test(
'generates a mock class but does not override methods w/ zero parameters',
@@ -68,7 +76,11 @@
dynamic a() => 7;
}
'''),
- _containsAllOf('class MockFoo extends _i1.Mock implements _i2.Foo {}'),
+ _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
);
});
@@ -80,7 +92,11 @@
int _b(int x) => 8;
}
'''),
- _containsAllOf('class MockFoo extends _i1.Mock implements _i2.Foo {}'),
+ _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
);
});
@@ -91,7 +107,11 @@
static int c(int y) => 9;
}
'''),
- _containsAllOf('class MockFoo extends _i1.Mock implements _i2.Foo {}'),
+ _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
);
});
@@ -104,7 +124,11 @@
}
class Foo {}
'''),
- _containsAllOf('class MockFoo extends _i1.Mock implements _i2.Foo {}'),
+ _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
);
});
@@ -185,8 +209,18 @@
},
outputs: {
'foo|test/foo_test.mocks.dart': _containsAllOf(
- 'class MockFoo extends _i1.Mock implements _i2.Foo {}',
- 'class MockBar extends _i1.Mock implements _i2.Bar {}',
+ dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ '''),
+ dedent('''
+ class MockBar extends _i1.Mock implements _i2.Bar {
+ MockBar() {
+ _i1.throwOnMissingStub(this);
+ }
+ }
+ '''),
),
},
);
@@ -211,8 +245,18 @@
},
outputs: {
'foo|test/foo_test.mocks.dart': _containsAllOf(
- 'class MockFoo extends _i1.Mock implements _i2.Foo {}',
- 'class MockBar extends _i1.Mock implements _i2.Bar {}',
+ dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ '''),
+ dedent('''
+ class MockBar extends _i1.Mock implements _i2.Bar {
+ MockBar() {
+ _i1.throwOnMissingStub(this);
+ }
+ }
+ '''),
),
},
);
@@ -237,8 +281,18 @@
},
outputs: {
'foo|test/foo_test.mocks.dart': _containsAllOf(
- 'class MockFoo extends _i1.Mock implements _i2.Foo {}',
- 'class MockBar extends _i1.Mock implements _i2.Bar {}',
+ dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ '''),
+ dedent('''
+ class MockBar extends _i1.Mock implements _i2.Bar {
+ MockBar() {
+ _i1.throwOnMissingStub(this);
+ }
+ }
+ '''),
),
},
);
@@ -249,8 +303,11 @@
dedent(r'''
class Foo<T, U> {}
'''),
- _containsAllOf(
- 'class MockFoo<T, U> extends _i1.Mock implements _i2.Foo<T, U> {}'),
+ _containsAllOf(dedent('''
+ class MockFoo<T, U> extends _i1.Mock implements _i2.Foo<T, U> {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
);
});
@@ -271,8 +328,18 @@
},
outputs: {
'foo|test/foo_test.mocks.dart': _containsAllOf(
- 'class MockFoo extends _i1.Mock implements _i2.Foo {}',
- 'class MockBar<T extends _i2.Foo> extends _i1.Mock implements _i2.Bar<T> {}',
+ dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ '''),
+ dedent('''
+ class MockBar<T extends _i2.Foo> extends _i1.Mock implements _i2.Bar<T> {
+ MockBar() {
+ _i1.throwOnMissingStub(this);
+ }
+ }
+ '''),
),
},
);
@@ -326,6 +393,10 @@
///
/// See the documentation for Mockito's code generation for more information.
class MockFoo extends _i1.Mock implements _i2.Foo {
+ MockFoo() {
+ _i1.throwOnMissingStub(this);
+ }
+
dynamic f(List<_i2.Foo>? list) =>
super.noSuchMethod(Invocation.method(#f, [list]));
}
@@ -360,6 +431,10 @@
///
/// See the documentation for Mockito's code generation for more information.
class MockFoo extends _i1.Mock implements _i2.Foo {
+ MockFoo() {
+ _i1.throwOnMissingStub(this);
+ }
+
dynamic f(_i2.Callback? c) => super.noSuchMethod(Invocation.method(#f, [c]));
dynamic g(_i2.Callback2? c) => super.noSuchMethod(Invocation.method(#g, [c]));
dynamic h(_i2.Callback3<_i2.Foo>? c) =>
@@ -612,77 +687,83 @@
});
test('does not override methods with all nullable parameters', () async {
- await _testWithNonNullable(
- {
- ...annotationsAsset,
- ...simpleTestAsset,
- 'foo|lib/foo.dart': dedent(r'''
- class Foo {
- void a(int? p) {}
- void b(dynamic p) {}
- void c(var p) {}
- void d(final p) {}
- void e(int Function()? p) {}
- }
- '''),
- },
- 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 {}
- '''),
- },
+ await _expectSingleNonNullableOutput(
+ dedent('''
+ class Foo {
+ void a(int? p) {}
+ void b(dynamic p) {}
+ void c(var p) {}
+ void d(final p) {}
+ void e(int Function()? p) {}
+ }
+ '''),
+ _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
);
});
test('does not override methods with a void return type', () async {
await _expectSingleNonNullableOutput(
- dedent(r'''
- abstract class Foo {
- void m();
- }
- '''),
- _containsAllOf('class MockFoo extends _i1.Mock implements _i2.Foo {}'),
+ dedent('''
+ abstract class Foo {
+ void m();
+ }
+ '''),
+ _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
);
});
test('does not override methods with an implicit dynamic return type',
() async {
await _expectSingleNonNullableOutput(
- dedent(r'''
- abstract class Foo {
- m();
- }
- '''),
- _containsAllOf('class MockFoo extends _i1.Mock implements _i2.Foo {}'),
+ dedent('''
+ abstract class Foo {
+ m();
+ }
+ '''),
+ _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
);
});
test('does not override methods with an explicit dynamic return type',
() async {
await _expectSingleNonNullableOutput(
- dedent(r'''
- abstract class Foo {
- dynamic m();
- }
- '''),
- _containsAllOf('class MockFoo extends _i1.Mock implements _i2.Foo {}'),
+ dedent('''
+ abstract class Foo {
+ dynamic m();
+ }
+ '''),
+ _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
);
});
test('does not override methods with a nullable return type', () async {
await _expectSingleNonNullableOutput(
- dedent(r'''
- abstract class Foo {
- int? m();
- }
- '''),
- _containsAllOf('class MockFoo extends _i1.Mock implements _i2.Foo {}'),
+ dedent('''
+ abstract class Foo {
+ int? m();
+ }
+ '''),
+ _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
);
});
@@ -710,17 +791,8 @@
'''),
},
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<T> extends _i1.Mock implements _i2.Foo<T> {
- void a(T? m) => super.noSuchMethod(Invocation.method(#a, [m]));
- }
- '''),
+ 'foo|test/foo_test.mocks.dart': _containsAllOf(
+ 'void a(T? m) => super.noSuchMethod(Invocation.method(#a, [m]));'),
},
);
});
@@ -746,6 +818,10 @@
///
/// See the documentation for Mockito's code generation for more information.
class MockFoo extends _i1.Mock implements _i2.Foo {
+ MockFoo() {
+ _i1.throwOnMissingStub(this);
+ }
+
dynamic f<T>(int? a) => super.noSuchMethod(Invocation.method(#f, [a]));
dynamic g<T extends _i2.Foo>(int? a) =>
super.noSuchMethod(Invocation.method(#g, [a]));
@@ -774,7 +850,11 @@
int? get m => 7;
}
'''),
- _containsAllOf('class MockFoo extends _i1.Mock implements _i2.Foo {}'),
+ _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
);
});
@@ -797,7 +877,11 @@
void set m(int? a) {}
}
'''),
- _containsAllOf('class MockFoo extends _i1.Mock implements _i2.Foo {}'),
+ _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
);
});
@@ -834,7 +918,11 @@
int? m;
}
'''),
- _containsAllOf('class MockFoo extends _i1.Mock implements _i2.Foo {}'),
+ _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
);
});
@@ -845,7 +933,11 @@
int _a;
}
'''),
- _containsAllOf('class MockFoo extends _i1.Mock implements _i2.Foo {}'),
+ _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
);
});
@@ -856,7 +948,11 @@
static int b;
}
'''),
- _containsAllOf('class MockFoo extends _i1.Mock implements _i2.Foo {}'),
+ _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
);
});
@@ -1166,6 +1262,10 @@
///
/// See the documentation for Mockito's code generation for more information.
class MockFoo extends _i1.Mock implements _i2.Foo {
+ MockFoo() {
+ _i1.throwOnMissingStub(this);
+ }
+
_i2.Bar m1() => super.noSuchMethod(Invocation.method(#m1, []), _FakeBar());
_i2.Bar m2() => super.noSuchMethod(Invocation.method(#m2, []), _FakeBar());
}
@@ -1628,8 +1728,13 @@
'''),
},
outputs: {
- 'foo|test/foo_test.mocks.dart': _containsAllOf(
- 'class MockFoo extends _i1.Mock implements _i2.Foo {}'),
+ 'foo|test/foo_test.mocks.dart': _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ MockFoo() {
+ _i1.throwOnMissingStub(this);
+ }
+ }
+ '''))
},
);
});
diff --git a/test/builder/custom_mocks_test.dart b/test/builder/custom_mocks_test.dart
index fbcfa1f..a7efc79 100644
--- a/test/builder/custom_mocks_test.dart
+++ b/test/builder/custom_mocks_test.dart
@@ -35,7 +35,10 @@
class MockSpec<T> {
final Symbol mockName;
- const MockSpec({Symbol as}) : mockName = as;
+ final bool returnNullOnMissingStub;
+
+ const MockSpec({Symbol as, this.returnNullOnMissingStub = false})
+ : mockName = as;
}
'''
};
@@ -58,6 +61,11 @@
'''
};
+const _constructorWithThrowOnMissingStub = '''
+MockFoo() {
+ _i1.throwOnMissingStub(this);
+ }''';
+
void main() {
test('generates a generic mock class without type arguments', () async {
await _testWithNonNullable(
@@ -74,9 +82,11 @@
'''
},
outputs: {
- 'foo|test/foo_test.mocks.dart': _containsAllOf(
- 'class MockFoo<T> extends _i1.Mock implements _i2.Foo<T> {}',
- ),
+ 'foo|test/foo_test.mocks.dart': _containsAllOf(dedent('''
+ class MockFoo<T> extends _i1.Mock implements _i2.Foo<T> {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
},
);
});
@@ -97,9 +107,13 @@
'''
},
outputs: {
- 'foo|test/foo_test.mocks.dart': _containsAllOf(
- 'class MockFooOfIntBool extends _i1.Mock implements _i2.Foo<int, bool> {}',
- ),
+ 'foo|test/foo_test.mocks.dart': _containsAllOf(dedent('''
+ class MockFooOfIntBool extends _i1.Mock implements _i2.Foo<int, bool> {
+ MockFooOfIntBool() {
+ _i1.throwOnMissingStub(this);
+ }
+ }
+ ''')),
},
);
});
@@ -120,9 +134,11 @@
'''
},
outputs: {
- 'foo|test/foo_test.mocks.dart': _containsAllOf(
- 'class MockFoo extends _i1.Mock implements _i2.Foo<int> {}',
- ),
+ 'foo|test/foo_test.mocks.dart': _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo<int> {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
},
);
});
@@ -143,9 +159,11 @@
'''
},
outputs: {
- 'foo|test/foo_test.mocks.dart': _containsAllOf(
- 'class MockFoo<T extends Object> extends _i1.Mock implements _i2.Foo<T> {}',
- ),
+ 'foo|test/foo_test.mocks.dart': _containsAllOf(dedent('''
+ class MockFoo<T extends Object> extends _i1.Mock implements _i2.Foo<T> {
+ $_constructorWithThrowOnMissingStub
+ }
+ ''')),
},
);
});
@@ -169,8 +187,18 @@
},
outputs: {
'foo|test/foo_test.mocks.dart': _containsAllOf(
- 'class MockFoo extends _i1.Mock implements _i2.Foo {}',
- 'class MockBar extends _i1.Mock implements _i2.Bar {}',
+ dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ '''),
+ dedent('''
+ class MockBar extends _i1.Mock implements _i2.Bar {
+ MockBar() {
+ _i1.throwOnMissingStub(this);
+ }
+ }
+ '''),
),
},
);
@@ -198,14 +226,50 @@
},
outputs: {
'foo|test/foo_test.mocks.dart': _containsAllOf(
- 'class MockAFoo extends _i1.Mock implements _i2.Foo {}',
- 'class MockBFoo extends _i1.Mock implements _i3.Foo {}',
+ dedent('''
+ class MockAFoo extends _i1.Mock implements _i2.Foo {
+ MockAFoo() {
+ _i1.throwOnMissingStub(this);
+ }
+ }
+ '''),
+ dedent('''
+ class MockBFoo extends _i1.Mock implements _i3.Foo {
+ MockBFoo() {
+ _i1.throwOnMissingStub(this);
+ }
+ }
+ '''),
),
},
);
});
test(
+ 'generates a mock class which uses the old behavior of returning null on '
+ 'missing stubs', () 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, returnNullOnMissingStub: true)])
+ void main() {}
+ '''
+ },
+ outputs: {
+ 'foo|test/foo_test.mocks.dart': _containsAllOf(dedent('''
+ class MockFoo<T> extends _i1.Mock implements _i2.Foo<T> {}
+ ''')),
+ },
+ );
+ });
+
+ test(
'throws when GenerateMock is given a class with a type parameter with a '
'private bound', () async {
_expectBuilderThrows(
@@ -384,8 +448,11 @@
'''),
},
outputs: {
- 'foo|test/foo_test.mocks.dart': _containsAllOf(
- 'class MockFoo extends _i1.Mock implements _i2.Foo {}'),
+ 'foo|test/foo_test.mocks.dart': _containsAllOf(dedent('''
+ class MockFoo extends _i1.Mock implements _i2.Foo {
+ $_constructorWithThrowOnMissingStub
+ }
+ '''))
},
);
});