Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Modularly stubbing consecutive method calls with Mockito

I am trying to use Mockito to mock a "Reader" type class. Think of a data stream reader, it has methods to read various data types and advance the internal pointer after each read.

public interface Reader {
    int readInt();
    short readShort();
}

The class being tested reads in various data structures from the data stream. For example,

public class Somethings {
    public List<Something> somethings;

    public Somethings(Reader reader) {
        somethings = new List<Something>();
        int count = reader.readInt();
        for (int i=0; i<count; i++) {
            somethings.add(readSomething(reader));
        }
    }

    private Something readSomething(Reader reader) {
        int a = reader.readInt();
        short b = reader.readShort();
        int c = reader.readInt();
        return new Something(a, b, c);
    }
}

And finally, I have my test:

public class SomethingsTest {
    @Test
    public void testSomethings() {
        Reader reader = Mockito.mock(Reader.class);

        readCount(reader, 2);
        readSomething(reader, 1, 2, 3);
        readSomething(reader, 4, 5, 6);

        Somethings somethings = new Somethings(reader);
        Assert.assertEqual(2, somethings.size());
        Assert.assertEquals(new Something(1, 2, 3), somethings.somethings.get(0));
        Assert.assertEquals(new Something(4, 5, 6), somethings.somethings.get(1));
    }

    private void readCount(Reader reader, int count) {
        when(reader.readInt()).thenReturn(count);
    }

    private void readSomething(Reader reader, int a, short b, int c) {
        when(reader.readInt()).thenReturn(a);
        when(reader.readShort()).thenReturn(b);
        when(reader.readInt()).thenReturn(c);
    }
}

Unfortunately, this does not work. reader.readInt() always returns 6 for every invocation. And I understand why it returns 6. That is not my question.

There are two options I can think of to fix the tests, but I don't particularly like either one.

The first option would be something like:

public class SomethingsTest {
    @Test
    public void testSomethings() {
        Reader reader = Mockito.mock(Reader.class);

        when(reader.readInt())
            .thenReturn(2)
            .thenReturn(1)
            .thenReturn(3)
            .thenReturn(4)
            .thenReturn(6);
        when(reader.readShort())
            .thenReturn(2)
            .thenReturn(5);

        Somethings somethings = new Somethings(reader);
        Assert.assertEqual(2, somethings.size());
        Assert.assertEquals(new Something(1, 2, 3), somethings.somethings.get(0));
        Assert.assertEquals(new Something(4, 5, 6), somethings.somethings.get(1));
    }
}

This should work, but it's very monolithic and messy. It's difficult to see which return is for which piece of which structure, because they're all intermixed, with no structure.

The second option I can think of is something like:

public class SomethingsTest {
    @Test
    public void testSomethings() {
        Reader reader = Mockito.mock(Reader.class);

        NewOngoingStubbing readIntStub = when(reader.readInt());
        NewOngoingStubbing readShortStub = when(reader.readShort());

        readCount(readIntStub, 2);
        readSomething(readIntStub, readShortStub, 1, 2, 3);
        readSomething(readIntStub, readShortStub, 4, 5, 6);

        Somethings somethings = new Somethings(reader);
        Assert.assertEqual(2, somethings.size());
        Assert.assertEquals(new Something(1, 2, 3), somethings.somethings.get(0));
        Assert.assertEquals(new Something(4, 5, 6), somethings.somethings.get(1));
    }

    private void readCount(NewOngoingStubbing readIntStub, int count) {
        readIntStub.thenReturn(count);
    }

    private void readSomething(NewOngoingStubbing readIntStub,
            NewOngoingStubbing  readShortStub, int a, short b, int c) {
        readIntStub.thenReturn(a);
        readShortStub.thenReturn(b);
        readIntStub.thenReturn(c);
    }
}

This at least maintains the structure of the original, but having to pass a separate object for each method call you want to make on the stubbed object is... ugh.

What would be the cleanest way to perform this test? Is there some option I'm missing here? Some functionality that I can leverage? I just started using Mockito tonight.. so I could very well be missing something.

like image 274
JesusFreke Avatar asked Oct 27 '12 10:10

JesusFreke


2 Answers

Mockito does ongoing stubbing natively. Your first example is fine, but this should also work:

when(reader.readInt()).thenReturn(2, 1, 3, 4, 6);

The documentation for it is here.

If you have something handling particularly complex interaction, it's OK to roll out your own stub class. You may find that initialising some fake with realistic data, then using that, provides a clearer example of how the classes collaborate than Mockito can. If that's the case, go with clarity over convention. Mockito is IMO the best mocking framework out there, but sometimes I still roll my own.

like image 187
Lunivore Avatar answered Oct 16 '22 21:10

Lunivore


When the standard methods for Mockito don't provide a mechanism to simulate your behavior, you can resort to implementing your own Answer. This is more work but provides extra flexibility.

Depending on your precise requirements, you could for instance create an Answer that returns a new element from a list of numbers, regardless of the request type (int or short). The variable readList can be a member that you can access from all the functions you use to set up your results.

final List<Integer> readList = new ArrayList<>();
// ... Fill readList with answers

Answer answerFromList = new Answer() {
    Object answer(InvocationOnMock invocation) {
        // Remove and return first element
        return readList.remove(0);
    }
}

when(reader.readInt()).thenAnswer(answerFromList);
when(reader.readShort()).thenAnswer(answerFromList);

Note that this Answer is a lot like the provided ReturnElementsOf, so you could use that directly as well.

like image 2
Thirler Avatar answered Oct 16 '22 20:10

Thirler