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
+        }
+        '''))
       },
     );
   });