Extend using run-time dummy values to Futures
If we have to create a `Future` of an unknown type, we have two
alternatives:
1. Create a fake class implementing `Future`. This is guaranteed
to fail if ever `await`ed, but works nice when setting
expectations.
2. Try to find a value for `T` at run-time and create a real
future. That can always fail if there is no dummy for `T`.
This change tries to unite these approaches:
- It adds `dummyValueOrNull` function that returns `null` instead
of throwing.
- If we need a value of type `Future<T>`, first try if we have a
dummy value for `T`. If it works, go with the real future,
otherwise fall back to faking.
PiperOrigin-RevId: 533386034
diff --git a/lib/src/builder.dart b/lib/src/builder.dart
index b94c689..7ac6562 100644
--- a/lib/src/builder.dart
+++ b/lib/src/builder.dart
@@ -1497,10 +1497,23 @@
if (typeArgument is analyzer.TypeParameterType &&
typeArgumentIsPotentiallyNonNullable) {
// We cannot create a valid Future for this unknown, potentially
- // non-nullable type, so we'll use a `_FakeFuture`, which will throw
+ // non-nullable type, so try creating a value at run-time and if
+ // that fails, we'll use a `_FakeFuture`, which will throw
// if awaited.
final futureType = typeProvider.futureType(typeArguments.first);
- return _dummyValueImplementing(futureType, invocation);
+ return referImported('ifNotNull', 'package:mockito/src/dummies.dart')
+ .call([
+ referImported('dummyValueOrNull', 'package:mockito/src/dummies.dart')
+ .call([refer('this'), invocation], {},
+ [_typeReference(typeArgument)]),
+ Method((b) => b
+ ..requiredParameters.add(Parameter((p) => p
+ ..type = _typeReference(typeArgument)
+ ..name = 'v'))
+ ..body = _futureReference(_typeReference(typeArgument))
+ .property('value')
+ .call([refer('v')]).code).closure
+ ]).ifNullThen(_dummyValueImplementing(futureType, invocation));
} else {
// Create a real Future with a legal value, via [Future.value].
final futureValueArguments = typeArgumentIsPotentiallyNonNullable
diff --git a/lib/src/dummies.dart b/lib/src/dummies.dart
index 9bf2dd0..9c6d8ca 100644
--- a/lib/src/dummies.dart
+++ b/lib/src/dummies.dart
@@ -125,7 +125,7 @@
Stream<Never>.empty(),
];
-T dummyValue<T>(Object parent, Invocation invocation) {
+T? dummyValueOrNull<T>(Object parent, Invocation invocation) {
if (null is T) return null as T;
final dummyBuilder = _dummyBuilders[T] ?? _defaultDummyBuilders[T];
if (dummyBuilder != null) {
@@ -135,6 +135,12 @@
if (value is DummyBuilder) value = value(parent, invocation);
if (value is T) return value;
}
+ return null;
+}
+
+T dummyValue<T>(Object parent, Invocation invocation) {
+ final value = dummyValueOrNull<T>(parent, invocation);
+ if (value is T) return value;
throw MissingDummyValueError(T);
}
@@ -153,3 +159,7 @@
void resetDummyBuilders() {
_dummyBuilders.clear();
}
+
+// Helper function.
+R? ifNotNull<T, R>(T? value, R Function(T) action) =>
+ value != null ? action(value) : null;
diff --git a/test/builder/auto_mocks_test.dart b/test/builder/auto_mocks_test.dart
index 12cccf1..ff6e713 100644
--- a/test/builder/auto_mocks_test.dart
+++ b/test/builder/auto_mocks_test.dart
@@ -2310,7 +2310,7 @@
Future<T> m<T>() async => false;
}
'''),
- _containsAllOf('returnValue: _FakeFuture_0<T>('),
+ _containsAllOf('dummyValueOrNull<T>(', '_FakeFuture_0<T>('),
);
});
diff --git a/test/end2end/foo.dart b/test/end2end/foo.dart
index e57b925..6e8770a 100644
--- a/test/end2end/foo.dart
+++ b/test/end2end/foo.dart
@@ -20,6 +20,7 @@
void returnsVoid() {}
Future<void> returnsFutureVoid() => Future.value();
Future<void>? returnsNullableFutureVoid() => Future.value();
+ Future<T> returnsFuture(T x) => Future.value(x);
Bar returnsBar(int arg) => Bar();
}
diff --git a/test/end2end/generated_mocks_test.dart b/test/end2end/generated_mocks_test.dart
index 62c9c7a..623ddcc 100644
--- a/test/end2end/generated_mocks_test.dart
+++ b/test/end2end/generated_mocks_test.dart
@@ -184,6 +184,11 @@
.having((e) => e.toString(), 'toString()', contains('getter'))),
);
});
+
+ test('a method returning Future<T> can be stubbed', () async {
+ when(foo.returnsFuture(any)).thenAnswer((_) async => 1);
+ expect(await foo.returnsFuture(0), 1);
+ });
});
group('for a generated mock using unsupportedMembers', () {
@@ -278,7 +283,7 @@
});
group('for a generated nice mock', () {
- late Foo<Object> foo;
+ late Foo<Bar> foo;
setUp(() {
foo = MockFooNice();
@@ -315,6 +320,28 @@
throwsA(isA<FakeUsedError>().having(
(e) => e.toString(), 'toString()', contains('returnsBar(43)'))));
});
+ group('a method returning Future<T>', () {
+ final bar = Bar();
+ tearDown(() {
+ resetMockitoState();
+ });
+ test('returns a fake future if unstubbed', () {
+ expect(foo.returnsFuture(bar), isA<SmartFake>());
+ });
+ test('returned fake future cannot be awaited', () async {
+ try {
+ await foo.returnsFuture(bar);
+ // Expect it to throw.
+ expect('This code should not be reached', false);
+ } catch (e) {
+ expect(e, isA<FakeUsedError>());
+ }
+ });
+ test('with provideDummy returned value can be awaited', () async {
+ provideDummy<Bar>(bar);
+ expect(await foo.returnsFuture(MockBar()), bar);
+ });
+ });
});
test('a generated mock can be used as a stub argument', () {