简体   繁体   中英

How to dynamically append results to a mocked method in Mockito

I'm originally mocking java.sql.ResultSet like this:

ResultSet rs = mock(ResultSet.class);
when(rs.next()).thenReturn(true, false);
when(rs.getString(1)).thenReturn("foo");
when(rs.getString(2)).thenReturn("bar");
when(rs.getInt(3)).thenReturn(55);

The above code is for mocking a simple one level data mocking, and it's working without any issues.

However, we have more complicated cases in our code where we use ResultSet in multiple method calling levels. Here's a sample code for a nested calling for ResultSet.

void level1(){
    ResultSet rs = stmt.executeQuery();
    while (rs.next()) {
        String a = rs.getString(1);//Expected to return "foo"
        String b = rs.getString(2);//Expected to return "bar"
        int c = rs.getInt(3);//Expected to return 55
    }
    level2();
}
void level2(){
    ResultSet rs = stmt.executeQuery();
    while (rs.next()) {
        String a = rs.getString(1);//Expected to return "lorem"
        String b = rs.getString(2);//Expected to return "ipsum"
    }
    level3();
}
void level3(){
    ResultSet rs = stmt.executeQuery();
    while (rs.next()) {
        String a = rs.getString(1);//Expected to return "alice"
        int b = rs.getInt(2);//Expected to return 66
        int c = rs.getInt(3);//Expected to return 77
    }
    level3();
}

To mock the method level1 in the above snippet, we have to write something similar to this

ResultSet rs = mock(ResultSet.class);
when(rs.next()).thenReturn(true, false, true, false, true, false);
when(rs.getString(1)).thenReturn("foo", "lorem", "alice");
when(rs.getString(2)).thenReturn("bar", "ipsum");
when(rs.getInt(3)).thenReturn(55, 77);
when(rs.getInt(2)).thenReturn(66);

As you can see from the above example, mocking the nested methods is not readable at all.

We are looking for a way to replace the unreadable mocking code with something more redable that will look like this:

ResultSet rs = mock(ResultSet.class);
when(rs.next()).thenReturnAndAppend(true, false);
when(rs.getString(1)).thenReturnAndAppend("foo");
when(rs.getString(2)).thenReturnAndAppend("bar");
when(rs.getInt(3)).thenReturnAndAppend(55);

when(rs.next()).thenReturnAndAppend(true, false);
when(rs.getString(1)).thenReturnAndAppend("lorem");
when(rs.getString(2)).thenReturnAndAppend("ipsum");

when(rs.next()).thenReturnAndAppend(true, false);
when(rs.getString(1)).thenReturnAndAppend("alice");
when(rs.getInt(2)).thenReturnAndAppend(66);
when(rs.getInt(3)).thenReturnAndAppend(77);

Is there a way in Mockito to achieve that?

I've already tried to use when().then() multiple times for the same method, but that overrides the previous mock for that method, and doesn't append values.

I've also tried to use OngoingStubbing to accomplish that but it throws an exception saying this is a bad behavior to use then on different lines.

I've also tried to create a custom mock method to store values in a map and load them from the map later

    private Map<Object, Stream> mockingMap;
    protected <T> void whenThen(T method, T value) {
        if(!mockingMap.containsKey(method)) {
            mockingMap.put(mock, (Stream<T>)Stream.of());
        }
        mockingMap.put(method, Stream.concat(mockingMap.get(method), Stream.of(value)))
        when(method).thenAnswer(e -> {
            mockingMap.get(method).next()
        });
    }

And will use this method like this:

whenThen(rs.getString(1), "foo");
whenThen(rs.getString(1), "lorem");

The issue with this implementation is that the value of method in whenThen method doesn't represent the mocked method rs.getString(1) , which means that calling rs.getString(1) two times will result in two different values for the method parameter.

My first question is: Is there any built in method in Mockito that has a similar behavior to thenReturnAndAppend . My second question is: If there's no similar method to thenReturnAndAppend , then how can I pass a unique key to the whenThen method that represents the method that we are trying to mock?

According to the documentation, you can do so using iterator-style stubbing :

https://javadoc.io/doc/org.mockito/mockito-core/2.24.5/org/mockito/Mockito.html#stubbing_consecutive_calls

Stubbing consecutive calls (iterator-style stubbing) Sometimes we need to stub with different return value/exception for the same method call. Typical use case could be mocking iterators. Original version of Mockito did not have this feature to promote simple mocking. For example, instead of iterators one could use Iterable or simply collections. Those offer natural ways of stubbing (eg using real collections). In rare scenarios stubbing consecutive calls could be useful, though:

 when(mock.someMethod("some arg"))
   .thenThrow(new RuntimeException())
   .thenReturn("foo");

 //First call: throws runtime exception:
 mock.someMethod("some arg");

 //Second call: prints "foo"
 System.out.println(mock.someMethod("some arg"));

 //Any consecutive call: prints "foo" as well (last stubbing wins).
 System.out.println(mock.someMethod("some arg"));
 

Your example would then look similar to:

PreparedStatement stmt = null;

ResultSet mockResult1 = Mockito.mock(ResultSet.class);
Mockito.when(mockResult1 .next()).thenReturn(true);
Mockito.when(mockResult1 .getString(1)).thenReturn("foo");
Mockito.when(mockResult1 .getString(2)).thenReturn("bar");
Mockito.when(mockResult1 .getInt(3)).thenReturn(55);


ResultSet mockResult2 = Mockito.mock(ResultSet.class);
Mockito.when(mockResult2 .next()).thenReturn(true);
Mockito.when(mockResult2 .getString(1)).thenReturn("lorem");
Mockito.when(mockResult2 .getString(2)).thenReturn("ipsum");
Mockito.when(mockResult2 .getInt(3)).thenReturn(55);


Mockito.when(stmt.executeQuery()).thenReturn(mockResult1).thenReturn(mockResult2);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM