Saturday, June 1, 2013

Mocking Iterable objects generically with Mockito


I often find my self having to mock iterable objects. At times I've created a test stub that overrides the iterator method and stubs the iterator, other times I have used mockito to mock the iterator. 

Most recently it was the Cassandra Datastax Java Driver's ResultSet object (which doesn't implement an interface and has a private constructor so you can't extend it and override or create an alternative implementation) which motivated me to create a generic method. 

So basically I want this code to work but with aSetOfStrings is a mock. You probably won't be mocking collections but it'll give you the idea.
         Set<String> aSetOfStrings = // passed in some how
       for (String s : aSetOfStrings) {
            results.add(s);
       }

And the method for creating the mock iterable object needs to be generic. E.g:
       public static <T> void mockIterable(Iterable<T> iterable, T... values)

Where the first parameter is the mock iterable object and the var arg is the objects that it should return. 

The foreach loop internally uses two methods on the iterator returned by the set's iterator() method: hasNext() and next().

So to get it to work three methods need to be mocked:
  1. Mock the iterable's (the set) iterator() method to return a mock iterator
  2. Mock the iterator's hasNext() method to return true N times followed by false where N is the number of values that you want the iterator to return.
  3. Mock the iterator's next() method to return the N elements in order.
Using mockito number one is easy:
        Iterator<T> mockIterator = mock(Iterator.class);
       when(iterable.iterator()).thenReturn(mockIterator);

Number two and three are slightly more complicated. Mockito lets you pass in a vararg for what to return. The slight complication is that the signature is:
        thenReturn(T, T...)

This is to enforce that at least one element is passed in. This means that for the hasNext() we need to pass in true N times followed by a false but the first true needs to be passed in separately rather then in the vararg. The same applies for next() - we can't simply use the vararg passed into our mockIterable(..) method we need to build a new array with N-1 elements in. This can be done as follows:

  1. If no values are passed in all we need to do is mock hasNext() to false.
  2. If a single value is passed in we don't need to build an array to pass into thenReturn.
  3. Finally, for more than one value we need to build the correct boolean array and values array to pass into thenReturn. For example:
MockIterator.mockIterable(mockIterable, "one", "two", "three");

We'd need the following mocking calls:
        when(mockIterable.hasNext()).thenReturn(true, [true, true, false]) 
and
        when(mockIterable.next()).thenReturn("one", ["two", "three"])

And here is the code to do it:
And the full code along with unit tests and a gradle script to build + pull in the dependencies is here.




2 comments:

Владимир Иванов said...

Thanks, now i know more=)

Unknown said...

This approach doesn't work well when we call hasNext() multiple times before calling next()(Iterator contract allows this).