-
Notifications
You must be signed in to change notification settings - Fork 166
Add strong mode-compliant 'typed' API #26
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
Conversation
Does the object on which we define behavior need to be the same as the object that implements the behavior? Maybe mocks can be created in pairs: one object on which we specify and verfiy the behavior from the test and one object we pass to the object under test. Then the object on which the behavior is defined doesn't need to implement the exact same interface as the mocked type but can accept matchers without causing errors. |
e6b4976
to
93396e4
Compare
OK this API is ready to merge, if we like it. There is a lot of code, but it is actually nicely contained:
|
@dskloetg : I think using two different objects would be a large break from the mockito API. Something that Mockito gives us for free is method signatures. For example, the analyzer complains about the following: class Cat {
bool eatFood(List<String> foods) => true;
}
class MockCat extends Mock implements Cat {}
void fn() {
var cat = new MockCat();
when(cat.eatFood()).thenReturn(true);
} Do you have a mockup of the API you had in mind? |
|
||
Unfortunately, the use of the arg matchers in mock method calls (like `cat.eatFood(any)`) | ||
violates the [Strong mode] type system. Specifically, if the method signature of a mocked | ||
method has a parameter with a parameterized type (like `List<int>`), then passing `any` or |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this also happen if the parameter has a simple type?
"Unsound implicit cast from dynamic to String"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so. This is why it's not such a prominent problem. Just a little prominent:
$ cat lib/dogs.dart
import 'mockito.dart';
class Dog {
bool eatFood(String food) => true;
bool eatFood2(List<String> foods) => true;
}
class MockDog extends Mock implements Dog {}
void fn() {
var dog = new MockDog();
when(dog.eatFood(any)).thenReturn(true);
when(dog.eatFood2(any)).thenReturn(true);
}
$ dartanalyzer --strong lib/dogs.dart
Analyzing [lib/dogs.dart]...
[warning] Unsound implicit cast from dynamic to List<String> (/Users/srawlins/code/dart-mockito/lib/dogs.dart, line 13, col 21)
1 warning found.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be curious to know why one is considered unsound but the other isn't.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any implicit or explicit cast might fail in DDC. Similarly, any implicit or explicit cast might fail in regular Dart checked mode. In general, for simple types (like String), if the cast passed in checked mode, it will pass in DDC. For complex types (like List<String>
), DDC may throw a type error where checked mode wouldn't have. This is because the checked mode check is unsound for composite types like List<String>
(a List<dynamic>
will be allowed through), and so DDC strengthens the cast. Since these are places where code that worked in checked mode may fail in DDC, we emit a special warnings for these casts that might fail in DDC where the would not have in checked mode.
// Return a new [Invocation], reconstituted from [invocation], [_typedArgs], | ||
// and [_typedNamedArgs]. | ||
Invocation _reconstituteInvocation(Invocation invocation) { | ||
var newInvocation = new FakeInvocation(invocation); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just return without temp variable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Just curious why an a new Eg
etc |
I'm on vacation so I won't get back to this for 2.5 weeks. You can treat my remaining comments as FYI. |
@Andersmholmgren It's mostly just backwards-compatibility issues. If we changed |
@TedSander This is ready to review/merge! |
So we don't want to change |
|
||
```dart | ||
when(cat.walk( | ||
typed/*<List<String>>*/(any), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure whether this approach is still under consideration, but I don't think you should need to pass these type arguments explicitly - they are in a position that should be inferrable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is. Sam is just checking to make sure it will work with DDC. Do you see any reason it won't @leafpetersen
Removing those comments would be a nice improvement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm understanding the overall approach here, I think this won't work without some compiler help. The way that DDC generates code means that you can only get to noSuchMethod by going through dynamic. So
dynamic cat = new MockCat();
cat.eatFood([]);
would work, but
var cat = new MockCat();
cat.eatFood([]);
}
won't, because we will infer that cat
has type MockCat
, and not generate the dynamic call. It's arguably a bug that we still allow noSuchMethod to suppress the warnings that you should get by not fully implementing the Cat
interface.
I think this might be a place where some language and/or compiler help might be appropriate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Leaf exactly what we wanted to know. Some language or compiler help would be appreciated. Even if there are no changes to the language having recommendations from the DDC team on how we should solve this would be appreciated. I think Dart should have a solution to stubbing/mocking for tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed a bug on the fact that we just silently accept this here:
Supporting this sort of thing is something I think we should do. I'd like to be sure we understand and cover all (or as many as possible) of the use cases though.
I can confirm that as of dart-archive/dev_compiler@e5c2aac, the |
What do you think of my last suggestion from 22 days ago? |
@dskloetg I'd have to make the following matching functions:
This converts two getters to functions, only so they can be used with named args 😦 . I think this could have some real consequences, as passing I think the single |
|
||
/// An Invocation implementation that allows all attributes to be passed into | ||
/// the constructor. | ||
class FakeInvocation extends Invocation { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not really in love with this name, and the comment doesn't seem to really apply anymore. To a user they can't pass in all the attributes. Would TypedInvocation be better, or InvocationForTypedArguments? Fake doesn't really tell me much here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. _InvocationForTypedArguments.
// `when(obj.fn(typed(any, name: 'a')))`. | ||
throw new ArgumentError( | ||
'A typed argument was declared with name $name, but was not passed ' | ||
'as an argument named $name.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the example would be useful in the error.
Maybe:
Bad: when(obj.fn(typed(any, name: 'a')))
Good: when(obj.fn(a: typed(any, name: 'a')))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
We should also update the minor version. Maybe while we are at it we should take it to 1.0.0 This code has been stable, and the dart team has asked us to reflect that fact. |
OK tests are good; new DDC tests in tool/. I'm fine for bumping to 1.0.0. Should I go for it? |
@@ -55,6 +60,139 @@ class Mock { | |||
String toString() => _givenName != null ? _givenName : runtimeType.toString(); | |||
} | |||
|
|||
// Return a new [Invocation], reconstituted from [invocation], [_typedArgs], | |||
// and [_typedNamedArgs]. | |||
Invocation _reconstituteInvocation(Invocation invocation) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a little overkill, why not just inline the new?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Update the version number to 1.0.0?? |
Done. Version bump included. |
Thanks Sam. Excited that we can now write DDC tests! |
Add strong mode-compliant 'typed' API
Fixes #25
CC @leafpetersen @kevmoo @TedSander
This solution is modeled after Java Mockito's implementation. Unfortunately, Java doesn't have named args, so this implementation is a little uglier with regards to named args.
The README and the tests show usage. What do you think?
Alternatives
@TedSander and I came up with the
typed/*<horror>*/(matcher, {name})
API. But we're not married to it, if people can think of something cleaner. Maybe a separatetypedNamed
?We could not conceive of a way for the API to understand when an argument is passed as a named argument. We can't rely on order (
when(foo.fn(a: typed(...), b: typed(...)))
), because Invocation's namedArguments is a Map, possibly with unstable order.