Does Fantom have any support for mocking classes and mixins? I've looked through the docs and didn't see anything, although I may have overlooked something.
This year I became a big fan of the Mockito library. Of all the mocking libraries I've tried, this one is the easiest to use and has the nicest syntax.
In general the nice thing about mocking is that it keeps your unit tests isolated. If you're testing class Foo and Foo depends on Bar, you just mock out Bar and only mock out the methods that need to return a result. After you've exercised Foo, you can verify the interactions you expected to happen on Bar:
Bar bar = Mockito.mock(Bar.class);
when(bar.getBaz()).thenReturn(Mockito.mock(Baz.class));
Foo foo = new Foo(bar);
foo.execute();
verify(bar, Mockito.times(1)).getBaz();
It's really nice being able to write your unit tests and only worry about the seams between your test class and its dependencies, and not the dependencies themselves.
I'm going to do some experiments with Mockito tonight to see how well they interoperate with Fantom. I'll let you know how it goes.
qualidafialMon 20 Sep 2010
Here's the result of my testing with Mockito thus far:
Mockito is able to mock non-final classes and methods. It is even able to mock methods not marked virtual--this is important since it allows you to mock out dependencies without the dependencies having to design every single method for polymorphism.
Lack of static import makes code somewhat more verbose. Typically in client code you import Mockito.* and that makes the test code more compact.
Generics support is an issue. For example in Java the expression:
when(foo.bar()).thenReturn(mock(Bar.class))
This works well because when(T) returns OngoingStubbing<T>, which allows you to chain the thenReturn(someT). Fantom doesn't retain this and so loses some type safety--not a big deal in this case.
However when you try something like:
verify(foo).bar()
Since the verify method is typed as <T> T verify(T) it is guaranteed to return whatever type is passed in, thus allowing method chaining.
I tried dynamic dispatch
verify(foo)->bar
But because that invokes the trap method, Mockito doesn't recognize it as equivalent and the verify fails.
Eventually I settled on (verify(foo) as Foo).bar and that worked correctly. However it's more verbose than it could be.
The last issue was a doozy. In Mockito you can call methods on mocks and provide explicit values for all your arguments, or you can use pattern matching, as in:
The problem in Fantom is that pattern matching methods all return nullary values--Fantom's defVal equivalent for primitives, and null for objects. This caused issues for verifying methods with non-nullable argument types. Some real code this time, from a data binding framework I'm working on:
Listeners listeners := Mockito.mock(Listeners#->toClass)
property.watch(obj)
(verify(listeners) as Listeners).add(
Mockito.eq(#property),
Mockito.any(Func#->toClass))
The method signature for the add method is:
Void add(Obj eventType, }Event| listener)
Both arguments are non-nullable. To get around the way Mockito works I use elvis expressions:
So mocking can be made to work in Fantom with Mockito. However you unfortunately have to code around some idiosyncrasies and knowledge of Mockito's internal workings.
yachrisMon 20 Sep 2010
Hey qualidafial,
Thanks for doing this -- I'm a big fan of Mocking. It's so cool to have fant built in, so that unit testing is trivial. I've done some mock classes by hand, but Mockito-style mocking would be fantastic.
For those of you who don't know about mocking (or for those who do), the Mock Roles, not Objects (warning: PDF) paper is really worth reading.
yachrisMon 20 Sep 2010
Andy and Brian...
I see that Mockito still can't mock static methods, any chance you could "build in" a mocking package, something like how fant is built in, so that we can mock static methods as well as ordinary ones? That would be fantastic... thanks!
I think Mockito is the one to follow, except for this language-specific issue.
qualidafialMon 20 Sep 2010
One idea I thought I'd throw out in response to this, is that it would be useful to have some sort of generic device for methods that return the same type as an input parameter. I suggest introducing a That psuedotype which implements a common idiom in Java generics:
<T> T verify(T obj)
In Fantom this could look like:
That verify(That obj)
I'm not sure how much effort it would be on the compiler side, but I think it would be a useful feature in this scenario and probably many others.
brianMon 20 Sep 2010
I like that - although part of my doesn't like building generic one-offs versus a full fledged generics system (not that I want to bite that off either :-)
qualidafialMon 20 Sep 2010
Posted #1222 to discuss That pseudotype separately.
Are there any plans (or proposals) for a universal generics feature available to all classes? It's good we have it for List, Map and Func but that can also be frustrating not being able to use it in our own APIs like we can in Java.
yachrisTue 21 Sep 2010
qualidafial,
I'm so close :-)
I've got:
Void testModified()
{
now := DateTime.now() + -1800sec
bef_time := now + -1800sec
aft_time := now + 1800sec
File file_bef := Mockito.mock(File#->toClass)
Mockito.when(file_bef.modified).thenReturn(bef_time)
File file_aft := Mockito.mock(File#->toClass)
Mockito.when(file_aft.modified).thenReturn(aft_time)
And it almost works... but fails when the actual code tries to access the modified field. Is there a way to mock fields? file_bef#modifed refuses to compile.
Also, is there a way to mock methods like File#walk which take a Func?
THANKS!
brianTue 21 Sep 2010
So why exactly does "->" fail? I don't know anything about Mockito, but assuming you could trap "trap", then seems like you can solve a lot of problems via "->" and custom hooks.
qualidafialTue 21 Sep 2010
When you call Mockito.mock(Class), you get back an instance of that class/interface where every nonfinal method has been stubbed. That includes the trap method, so without stubbing trap to call the real method, any invocations using -> are a no-op.
brianTue 21 Sep 2010
But if you wrapped Mockito.mock with something that stubbed trap, would that work?
qualidafialWed 22 Sep 2010
As long as you're not trying to stub the trap method itself, probably.
yachrisWed 6 Oct 2010
Hoping for a little help here... I don't understand the explanations above, evidently.
My fake class:
class Foo
{
new make(Int indexVal)
{
index = indexVal
}
Int first(Int[] someInts)
{
return someInts[index]
}
Int index
}
The attempted test:
using [java]org.mockito::Mockito
class FooTest : Test
{
Void testFoo()
{
actualFoo := Foo(0)
Foo foo := Mockito.spy(actualFoo)
Mockito.when(foo.first(Mockito.any(Int[]#->toClass ?: [0]))).thenReturn(111)
verifyEq(111, foo.first([5, 111, 8192]))
verifyEq(111, foo.first([5555, 111, 8]))
(Mockito.verify(foo, Mockito.times(2)) as Foo).first([0])
}
}
And... it doesn't work. Replacing the spy with a mock makes no difference.
qualidafialWed 6 Oct 2010
Your elvis expressions should be outside the Mockito.any method:
using [java]org.mockito::Mockito
class FooTest : Test
{
Void testFoo()
{
Foo foo := Mockito.mock(Foo#->toClass)
Mockito.when(foo.first(Mockito.any(Int[]#->toClass) ?: [,])).thenReturn(111)
// Here ^^^^^^^^^
verifyEq(111, foo.first([5, 111, 8192]))
verifyEq(111, foo.first([5555, 111, 8]))
(Mockito.verify(foo, Mockito.times(2)) as Foo).first([0])
}
}
With this change the test passes on my machine
Edit: Fix Mockito.mock arguments
qualidafialThu 7 Oct 2010
As a follow-up to my last comment:
The reason you have to elvis on the result of Mockito.any is because that method returns "falseys," as the project members call it: false for Bool, 0 for numbers, and null for all objects.
This is a problem if a particular method argument is not nullable. Therefore we use elvis expressions to substitute a non-null value for the null from Mockito.any. Now the Fantom runtime doesn't (rightfully) bitch at us for passing null.
This also means that the elvis expression can be omitted when the method argument is nullable:
class Foo
{
...
Int? first(Obj[]? someInts) {
return someInts?.get(index)
}
}
class FooTest : Test
{
Void testFoo()
{
...
// no elvis, no problem
Mockito.when(foo.first(Mockito.any(Int[]#->toClass))).thenReturn(111)
...
}
}
The reason this works is that all invocations to argument matcher methods like any or eq cause those patterns to be internally stored in Mockito's argument accumulator. When a mock receives a method call, Mockito checks to see if there are any accumulated argument matchers.
In the context of a when phrase, those arguments are used to configure the mock for future behavior.
In the context of a verify phrase, those argument are compared against the normal invocations.
Mockito returns falseys since when the method call is received on the mock those arguments are going to be thrown away anyway in favor of the argument matchers. However we have to turn that null into a valid non-null object in our case
yachrisThu 7 Oct 2010
Wow, that's awesome, thanks for the fix and the explanation! Much appreciated.
qualidafial Sun 19 Sep 2010
Does Fantom have any support for mocking classes and mixins? I've looked through the docs and didn't see anything, although I may have overlooked something.
This year I became a big fan of the Mockito library. Of all the mocking libraries I've tried, this one is the easiest to use and has the nicest syntax.
In general the nice thing about mocking is that it keeps your unit tests isolated. If you're testing class Foo and Foo depends on Bar, you just mock out Bar and only mock out the methods that need to return a result. After you've exercised Foo, you can verify the interactions you expected to happen on Bar:
It's really nice being able to write your unit tests and only worry about the seams between your test class and its dependencies, and not the dependencies themselves.
I'm going to do some experiments with Mockito tonight to see how well they interoperate with Fantom. I'll let you know how it goes.
qualidafial Mon 20 Sep 2010
Here's the result of my testing with Mockito thus far:
Mockito is able to mock non-final classes and methods. It is even able to mock methods not marked virtual--this is important since it allows you to mock out dependencies without the dependencies having to design every single method for polymorphism.
Lack of static import makes code somewhat more verbose. Typically in client code you
import Mockito.*
and that makes the test code more compact.Generics support is an issue. For example in Java the expression:
This works well because
when(T)
returnsOngoingStubbing<T>
, which allows you to chain thethenReturn(someT)
. Fantom doesn't retain this and so loses some type safety--not a big deal in this case.However when you try something like:
Since the
verify
method is typed as<T> T verify(T)
it is guaranteed to return whatever type is passed in, thus allowing method chaining.I tried dynamic dispatch
But because that invokes the
trap
method, Mockito doesn't recognize it as equivalent and the verify fails.Eventually I settled on
(verify(foo) as Foo).bar
and that worked correctly. However it's more verbose than it could be.The last issue was a doozy. In Mockito you can call methods on mocks and provide explicit values for all your arguments, or you can use pattern matching, as in:
The problem in Fantom is that pattern matching methods all return nullary values--Fantom's defVal equivalent for primitives, and null for objects. This caused issues for verifying methods with non-nullable argument types. Some real code this time, from a data binding framework I'm working on:
The method signature for the
add
method is:Both arguments are non-nullable. To get around the way Mockito works I use elvis expressions:
So mocking can be made to work in Fantom with Mockito. However you unfortunately have to code around some idiosyncrasies and knowledge of Mockito's internal workings.
yachris Mon 20 Sep 2010
Hey qualidafial,
Thanks for doing this -- I'm a big fan of Mocking. It's so cool to have
fant
built in, so that unit testing is trivial. I've done some mock classes by hand, but Mockito-style mocking would be fantastic.For those of you who don't know about mocking (or for those who do), the Mock Roles, not Objects (warning: PDF) paper is really worth reading.
yachris Mon 20 Sep 2010
Andy and Brian...
I see that Mockito still can't mock static methods, any chance you could "build in" a mocking package, something like how
fant
is built in, so that we can mock static methods as well as ordinary ones? That would be fantastic... thanks!I think Mockito is the one to follow, except for this language-specific issue.
qualidafial Mon 20 Sep 2010
One idea I thought I'd throw out in response to this, is that it would be useful to have some sort of generic device for methods that return the same type as an input parameter. I suggest introducing a
That
psuedotype which implements a common idiom in Java generics:In Fantom this could look like:
I'm not sure how much effort it would be on the compiler side, but I think it would be a useful feature in this scenario and probably many others.
brian Mon 20 Sep 2010
I like that - although part of my doesn't like building generic one-offs versus a full fledged generics system (not that I want to bite that off either :-)
qualidafial Mon 20 Sep 2010
Posted #1222 to discuss
That
pseudotype separately.Are there any plans (or proposals) for a universal generics feature available to all classes? It's good we have it for List, Map and Func but that can also be frustrating not being able to use it in our own APIs like we can in Java.
yachris Tue 21 Sep 2010
qualidafial,
I'm so close :-)
I've got:
And it almost works... but fails when the actual code tries to access the
modified
field. Is there a way to mock fields?file_bef#modifed
refuses to compile.Also, is there a way to mock methods like
File#walk
which take aFunc
?THANKS!
brian Tue 21 Sep 2010
So why exactly does "->" fail? I don't know anything about Mockito, but assuming you could trap "trap", then seems like you can solve a lot of problems via "->" and custom hooks.
qualidafial Tue 21 Sep 2010
When you call
Mockito.mock(Class)
, you get back an instance of that class/interface where every nonfinal method has been stubbed. That includes thetrap
method, so without stubbingtrap
to call the real method, any invocations using->
are a no-op.brian Tue 21 Sep 2010
But if you wrapped Mockito.mock with something that stubbed trap, would that work?
qualidafial Wed 22 Sep 2010
As long as you're not trying to stub the trap method itself, probably.
yachris Wed 6 Oct 2010
Hoping for a little help here... I don't understand the explanations above, evidently.
My fake class:
The attempted test:
And... it doesn't work. Replacing the spy with a mock makes no difference.
qualidafial Wed 6 Oct 2010
Your elvis expressions should be outside the
Mockito.any
method:With this change the test passes on my machine
Edit: Fix Mockito.mock arguments
qualidafial Thu 7 Oct 2010
As a follow-up to my last comment:
The reason you have to elvis on the result of
Mockito.any
is because that method returns "falseys," as the project members call it: false for Bool, 0 for numbers, and null for all objects.This is a problem if a particular method argument is not nullable. Therefore we use elvis expressions to substitute a non-null value for the null from
Mockito.any
. Now the Fantom runtime doesn't (rightfully) bitch at us for passing null.This also means that the elvis expression can be omitted when the method argument is nullable:
The reason this works is that all invocations to argument matcher methods like
any
oreq
cause those patterns to be internally stored in Mockito's argument accumulator. When a mock receives a method call, Mockito checks to see if there are any accumulated argument matchers.In the context of a
when
phrase, those arguments are used to configure the mock for future behavior.In the context of a
verify
phrase, those argument are compared against the normal invocations.Mockito returns falseys since when the method call is received on the mock those arguments are going to be thrown away anyway in favor of the argument matchers. However we have to turn that null into a valid non-null object in our case
yachris Thu 7 Oct 2010
Wow, that's awesome, thanks for the fix and the explanation! Much appreciated.