Add strong mode-compliant 'typed' API#26
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.
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.
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.
I'd be curious to know why one is considered unsound but the other isn't.
There was a problem hiding this comment.
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.
lib/mockito.dart
Outdated
| // Return a new [Invocation], reconstituted from [invocation], [_typedArgs], | ||
| // and [_typedNamedArgs]. | ||
| Invocation _reconstituteInvocation(Invocation invocation) { | ||
| var newInvocation = new FakeInvocation(invocation); |
|
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 |
README.md
Outdated
|
|
||
| ```dart | ||
| when(cat.walk( | ||
| typed/*<List<String>>*/(any), |
There was a problem hiding this comment.
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.
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.
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.
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.
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 |
lib/mockito.dart
Outdated
|
|
||
| /// An Invocation implementation that allows all attributes to be passed into | ||
| /// the constructor. | ||
| class FakeInvocation extends Invocation { |
There was a problem hiding this comment.
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.
Done. _InvocationForTypedArguments.
lib/mockito.dart
Outdated
| // `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.
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')))
|
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? |
lib/src/mock.dart
Outdated
|
|
||
| // Return a new [Invocation], reconstituted from [invocation], [_typedArgs], | ||
| // and [_typedNamedArgs]. | ||
| Invocation _reconstituteInvocation(Invocation invocation) => |
There was a problem hiding this comment.
This seems like a little overkill, why not just inline the new?
|
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
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.