Mockito Iterator korrekt mocken

JonnyRico

Mitglied
Hi,

ich habe das Problem, dass ich das exakte Verhalten eines Iterator-Objektes nicht nachgestellt bekommen.
Hier das folgende Beispiel:

Java:
public class IteratorTest {

    @Test
    public void unmocked() {
        List<String> list = new LinkedList<String>() {
            {
                add("String 1");
                add("String 2");
            }
        };
        assertEquals("String 1String 2String 1String 2", buildString(list));
    }
    
    @Test
    public void iteratorAndListMock() {
        Iterator mockIter = mock(Iterator.class);
        when(mockIter.hasNext()).thenReturn(true, true, false);
        when(mockIter.next()).thenReturn("String 1", "String 2");
        List<String> mockList = mock(List.class);
        when(mockList.iterator()).thenReturn(mockIter);
        
        assertEquals("String 1String 2String 1String 2", buildString(mockList));
    }
    
    private String buildString(List<String> list){
        StringBuilder res = new StringBuilder();
        for (String string : list) {
            res.append(string);
        }
        for (String string : list) {
            res.append(string);
        }
        return res.toString();
    }
}

In der Methode in der ich den gemockten Iterator verwende, schlägt die Assertion fehl, da im Resultat nur "String 1String 2" steht. Die zweite For-each in buildString() wird also nicht ausgeführt bzw. der Iterator steht am Ende der Liste. Kann mir irgendjemand sagen wie man den Iterator wirklich korrekt mocken kann?
1000 Dank im Voraus.

Gruß

Jonny
 
Hi,

ich habe jetzt noch mal ein bissel gehackt und bin zu diesem Ergebnis gekommen. Der Iterator wir quasi auf einen indizierten Zugriff umgebogen. Das ganze ist nicht schön und ich wäre auch echt sehr dankbar über eine bessere Lösung. So läuft es:

Java:
private int nextCalls = 0;
    private int hasNextCalls = 0;
    private static final String EXPECTED_RESULT = "String 1String 2String 3String 1String 2String 3String 1String 2String 3";
    List<String> list = new LinkedList<String>() {
        
        {
            add("String 1");
            add("String 2");
            add("String 3");
        }
    };
    
    @Test
    public void unmocked() {
        assertEquals(EXPECTED_RESULT, buildString(list));
    }
    
    @Test
    public void iteratorAndListMock() {        
        Iterator mockIter = mock(Iterator.class);
        
        when(mockIter.hasNext()).thenAnswer(new Answer<Boolean>() {
            
            @Override
            public Boolean answer(InvocationOnMock invocation) throws Throwable {
                ++hasNextCalls;
                if (hasNextCalls > list.size()) {
                    hasNextCalls = 0;
                    return false;
                }
                return true;
            }
        });
        when(mockIter.next()).thenAnswer(new Answer<String>() {
            
            @Override
            public String answer(InvocationOnMock invocation) throws Throwable {
                return list.get(nextCalls++ % list.size());
            }
        });
        List<String> mockList = mock(List.class);
        
        when(mockList.iterator()).thenReturn(mockIter);
        assertEquals(EXPECTED_RESULT, buildString(mockList));
    }
    
    private String buildString(List<String> list) {
        StringBuilder res = new StringBuilder();
        for (String string : list) {
            res.append(string);
        }
        for (String string : list) {
            res.append(string);
        }
        for (String string : list) {
            res.append(string);
        }
        return res.toString();
    }
 
Ich glaube, dass du von thenReturn ein anderes Verhalten erwartest. thenReturn(true, true, false) bedeutet, dass der 1. Call true liefert, der 2. Call ebenfalls und der dritte und alle weiteren Calls liefern false. Daher passiert in der zweiten for-Schleife gar nichts, da der Iterator für hasNext sofort false liefert. Das gleiche Problem gibt es bei thenReturn("String 1", "String 2"), denn da wird für den zweiten und jeden weiteren Call "String 2" zurückgeliefert.

Ich sehe da erstmal zwei Lösungsmöglichkeiten (neben deiner): Du machst ein thenReturn(true, true, false, true, true, false) daraus (analog für next) oder du erstellst dir einen zweiten Iterator-Mock mit gleichem Verhalten, den du beim zweiten iterator() Aufruf zurückgibst.
 
ohne das Framework zu kennen:
wie macht man es denn, zwischen ersten und zweiten Iterator-Aufruf zu unterscheiden?
der Code dazu ist doch anscheinend einfach nur
> when(mockList.iterator()).thenReturn(mockIter);

ok, da kann man vermuten dass die Rückgabe immer dasselbe eine Objekt ist, stattdessen bräuchte man die Rückgabe immer eines neuen Iterator-Objektes,
das ist ja ganz klar, soll nicht nur für den Test mit ZWEI Aufrufen funktionieren sondern generell beliebig oft,
wie formuliert man das, falls möglich?

> thenReturn(true, true, false, true, true, false)
steht als Lösung entsprechend völlig außer Frage, genauso eventuelles
> when(mockList.iterator()).thenReturn(mockIter, mockIter2);
 
@SlaterB Korrekt die Lösungen einer hart kodierten Anzahl von Rückgabewerten scheidet aus. Auch meine zweite Lösung funktioniert nur genau dann, wenn die Liste immer bis zum Ende durchlaufen wird und next und hasNext damit synchron sind. Was allerdings jetzt passen sollte ich die folgende, wenn durch den permanenten Neuaufbeu einer Liste vielleicht nicht die performanteste Lösung ist diese:

Java:
private static final String EXPECTED_RESULT = "String 1String 2String 3String 1String 2String 3String 1String 2String 3";
    private static final List<String> LIST = new LinkedList<String>() {
        
        {
            add("String 1");
            add("String 2");
            add("String 3");
        }
    };
    
    @Test
    public void unmocked() {
        assertEquals(EXPECTED_RESULT, buildString(LIST));
    }
    
    
    @Test
    public void iteratorAndListMock() {
        List<String> mockList = mock(List.class);
        when(mockList.iterator()).thenAnswer(new Answer<Iterator>() {

            @Override
            public Iterator answer(InvocationOnMock invocation) throws Throwable {
                return new ArrayList<String> (LIST).iterator();
            }
        });
        
        assertEquals(EXPECTED_RESULT, buildString(mockList));
    }
    
    private String buildString(List<String> list) {
        StringBuilder res = new StringBuilder();
        for (String string : list) {
            res.append(string);
        }
        for (String string : list) {
            res.append(string);
        }
        for (String string : list) {
            res.append(string);
        }
        return res.toString();
    }
 
Rein gefühlsmässig würde ich sagen, dass das kein Anwendungsfall für Mocks ist.

Ansonsten, das hier bitte nicht verwenden:
Java:
private static final List<String> LIST = new LinkedList<String>() {
        
        {
            add("String 1");
            add("String 2");
            add("String 3");
        }
    };
Du erstellst eine neue Implementierung der LinkedList, das willst du nicht 😉

Macht man so:
Java:
private static final List<String> list = Arrays.asList("String 1", "String 2", "String 3");
 
Zuletzt bearbeitet von einem Moderator:

Zurück
Oben