-
Notifications
You must be signed in to change notification settings - Fork 166
Mixin solution for unknown generic return type #342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Is this a working workaround? |
Mockito 5.1.0 contains a nice solution with a |
In my work on private field promotion (dart-lang/language#2020), I've discovered a new use case for this feature, and I think we should consider implementing it after all. First some background: the goal of private field promotion is to allow code like this to work: class A {
final int? _x;
...
}
void f(A a) {
if (a._x != null) {
print(a._x + 1); // Ok, because `a._x` is not null.
}
} In order for this to be sound, the compiler has to check if there's a class anywhere that implements class B implements A {
bool _b = false;
Object? noSuchMethod(_) => (_b = !_b) ? 1 : null;
} In effect, noSuchMethod opens up a loophole allowing classes outside of a library to override the implementation of a library's private members in a way that might violate the assumptions made by the library author. We have to close this loophole in order for private field promotion to be sound, but we would like to close it anynow, because in essence, it's a flaw in Dart's privacy system. The way we plan to close the loophole is by ensuring that an attept to invoke The consequence to Mockito is that it's no longer possible for tests to use Mockito's // lib1.dart
class C {
int get value => ...;
Object? _other;
}
int foo(C? c) {
c._other = ...;
return c.value;
}
// test.dart
import 'package:mockito/mockito.dart';
import 'lib1.dart';
@GenerateMocks([], customMocks: [
MockSpec<C>(returnNullOnMissingStub: true),
])
void main() {
test('foo', () {
final mockC = MockC();
when(mockC.value).thenReturn(1);
expect(foo(mockCurrentPlaceModel), 1);
});
} The purpose of the test is to verify that The way I'm trying to fix this in the internal code base is to create a mixin in the library containing the private name, and use it in the mock class, e.g.: // lib1.dart
class C {
int get value => ...;
Object? _other;
}
@visibleForTesting
mixin MockCMixin implements C {
@override
Object? _other;
}
int foo(C? c) {
c._other = ...;
return c.value;
}
// test.dart
import 'package:mockito/mockito.dart';
import 'lib1.dart';
class MockC extends Mock with MockCMixin implements C {
@override
int get value => (super.noSuchMethod(Invocation.getter(#value), returnValue: 0) as int;
}
void main() {
test('foo', () {
final mockC = MockC();
when(mockC.value).thenReturn(1);
expect(foo(mockCurrentPlaceModel), 1);
});
} But unfortunately, in order to do this, I can no longer take advantage of Mockito codegen. In particular, I have to override the @GenerateMocks([], customMocks: [
MockSpec<C>(returnNullOnMissingStub: true, mixingIn: MockCMixin),
]) |
FWIW I consider it a bug that mockito allows this behavior today. A library that uses private members like this does not support mocking for those values, and it's an accident that it happens to work today. If we need to allow the continued bad pattern to unblock migrations that could be an OK tradeoff, but it's not a pattern I'm happy to support. |
I didn't see your comment there, @natebosch, but you bring up a good point. I think I can prototype a breaking change where we define, in a generated mock class:
and if that's false, then in I can see how breaking this would be for internal google3. If it's expensive, we might have to kick that breaking change down the road... |
IIRC we've had discussions about removing this capability at the language level. Knowing how breaking it is for mockito users would be a good data point. @leafpetersen - I can't seem to find a language issue but I think we've discussed how allowing a Edit: Is it perhaps dart-lang/sdk#47923 ? Do I understand correctly that it will change some noSuchMethod "forwarder" in to noSuchMethod "thrower"? |
The relevant discussion is centralized here. |
This is primarily here to support private field promotion: dart-lang/language#2020 Also discussion at dart-lang/language#2275 The broad stroke is that users may need to start declaring little mixins next to their base classes with implementations for this or that private API which is (intentionally or not) accessed against a mock instance during a test. Fixes #342 PiperOrigin-RevId: 461933542
This is primarily here to support private field promotion: dart-lang/language#2020 Also discussion at dart-lang/language#2275 The broad stroke is that users may need to start declaring little mixins next to their base classes with implementations for this or that private API which is (intentionally or not) accessed against a mock instance during a test. Fixes #342 PiperOrigin-RevId: 461933542
This is primarily here to support private field promotion: dart-lang/language#2020 Also discussion at dart-lang/language#2275 The broad stroke is that users may need to start declaring little mixins next to their base classes with implementations for this or that private API which is (intentionally or not) accessed against a mock instance during a test. Fixes #342 PiperOrigin-RevId: 461933542
This is primarily here to support private field promotion: dart-lang/language#2020 Also discussion at dart-lang/language#2275 The broad stroke is that users may need to start declaring little mixins next to their base classes with implementations for this or that private API which is (intentionally or not) accessed against a mock instance during a test. Fixes #342 PiperOrigin-RevId: 461933542
This is primarily here to support private field promotion: dart-lang/language#2020 Also discussion at dart-lang/language#2275 The broad stroke is that users may need to start declaring little mixins next to their base classes with implementations for this or that private API which is (intentionally or not) accessed against a mock instance during a test. Fixes dart-lang/mockito#342 PiperOrigin-RevId: 461933542
Background
In #338 (and #339), @MaxAuer wants to stub a generic method with a return type which is tied to a type parameter on the method. Something like:
Mockito cannot generate a mock class for C because it must be able to return valid values in
add
, but cannot create values for unknown type variableT
. I'd like to explore one solution here, allowing the user to write a mixin which is mixed into the generated class. The mixin would implement one or more methods, and the generated class would not override those implementations with its own.Proof of Concept
I started a PoC API which would allow the following:
(I think the mixin has to have both superclass constraints, Foo, and Mock, in order to call
super.noSuchMethod
inside; can explore that later.)This generates:
(Mockito codegen awkwardly needs to generate two classes because declaring
class MockFoo extends Mock with M implements C {}
results in an error, "M can't be mixed onto Mock because Mock doesn't implement C.")The big problem
OK. All well and good. I wrote the API with the
MixinSpec<M>()
so that a user could specify type arguments if they so chose. Something likemixingIn: MixinSpec<M<int>>()
. The problem is that I don't see any way to mix a generic mixin onto a generic mock class. Take this simple example:What API would let you specify, "Uh, just tie the type parameter on M to the type parameter on MockFoo?" What about a slightly more complicated arrangement:
I don't see an easy way to specify generics. :( The good news is this should be unfathomably bizarre and rare, to want to specify your mixin like this.
Perhaps the best way forward is to just specify the mixin class with a symbol (
mixingIn: #M
), and never specify type arguments (instantiate to bounds).CC @matanlurey @TedSander @natebosch
The text was updated successfully, but these errors were encountered: