#1220 Fantom unit testing with mocks

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:

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.

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:

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:

verify(foo).bar(1, "string") // explicit
verify(foo).bar(eq(1), any(String.class))

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:

(verify(listeners) as Listeners).add(
    Mockito.eq(#property) ?: #property,
    Mockito.any(Func#->toClass) ?: |->| {} )

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:

<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.

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:

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!

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 the trap method, so without stubbing trap 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:

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.

qualidafial Wed 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

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:

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

yachris Thu 7 Oct 2010

Wow, that's awesome, thanks for the fix and the explanation! Much appreciated.

Login or Signup to reply.