Mockito FindIterable<Document>
Asked Answered
N

4

8

Am trying to write a JUnit test case for below method, am using Mockito framework.

Method:

public EmplInfo getMetaData(String objectId) {

        objectId = new StringBuffer(objectId).reverse().toString();
        try{
            BasicDBObject whereClauseCondition = getMetaDataWhereClause(objectId);
            EmplInfo emplinfo= new EmplInfo ();
            emplinfo.set_id(objectId);
            FindIterable<Document> cursorPersonDoc = personDocCollection.find(whereClauseCondition);
            for (Document doc : cursorPersonDoc) {
                emplinfo.setEmplFirstName(doc.getString("firstname"));
                emplinfo.setEmplLastName(doc.getString("lastname"));
                break;
            }
            return emplinfo;
        }catch(Exception e){
         e.printstacktrace();
        }

Junit:

@Test
public void testGetMetaData() throws Exception {
    String docObjectId = "f2da8044b29f2e0a35c0a2b5";
    BasicDBObject dbObj = personDocumentRepo.getMetaDataWhereClause(docObjectId);
    FindIterable<Document> findIterable = null;
    Mockito.when(collection.find(dbObj)).thenReturn(findIterable);
    personDocumentRepo.getMetaData(docObjectId);
}

Am getting null point expection in "personDocumentRepo.getMetaData(docObjectId)", because am "Return" the findIterable which is NULL. Not sure how to assign dummy/test value into findIterable.

Please advise.

Thanks! Bharathi

Napier answered 22/6, 2018 at 12:28 Comment(6)
Questions seeking debugging help ("why isn't this code working?") must include the desired behavior, a specific problem or error and the shortest code necessary to reproduce it in the question itself. Questions without a clear problem statement are not useful to other readers. See: How to create a minimal reproducible example. Use the edit link to improve your question - do not add more information via comments. Thanks!Parathion
What is missing for example: the definition of personDocumentRepo in your test class. Where is that coming from? And what is the point of setting findIterable() to null? Also note: you don't understand what when().thenReturn() is doing. There is no point in passing new Document() for a mocking spec. Instead, you want to pass a matcher, like when(any()) to indicate that you don't are what gets passed. But then: when you want to return null on any parameter, then you dont need a spec at all. Because that is what mockito does by default. Long story short:Parathion
It seems that you intend to do really advanced things, without understand the basics of using Mockito. I seriously recommend that you first read a tutorial about basic mockito, so that you understand the code you are writing down. Instead of writing down something and assuming it does this or that.Parathion
Oops, updated the question.Napier
Please don't use StringBuffer as it was replaced by StringBuilder in 2004.Catlin
These comments are a waste of time for who is trying to search a solution about this question.Metcalf
C
7

As you have rightly pointed out, you are getting NPE because FindIterable is null. You need to mock it.
Mocking it is not so straightforward, since it uses MongoCursor(this in turn extend Iterator), you need to mock certain methods which are used internally.

While traversing certain methods of the Iter

I believe you have to do something like this.

FindIterable iterable = mock(FindIterable.class);
MongoCursor cursor = mock(MongoCursor.class);

Document doc1= //create dummy document;
Document doc2= //create dummy document;

when(collection.find(dbObj)).thenReturn(iterable);

when(iterable.iterator()).thenReturn(cursor);
when(cursor.hasNext()) 
  .thenReturn(true)
  .thenReturn(true)// do this as many times as you want your loop to traverse
 .thenReturn(false); // Make sure you return false at the end.
when(cursor.next())
  .thenReturn(doc1)
  .thenReturn(doc2); 

This is not a complete solution. You need to adapt it to your class.

Coat answered 22/6, 2018 at 12:51 Comment(2)
This way to mock the iterator can be strongly simplified by creating a List and by passing List.iterator() in the mock recording. I added it in my answer.Ibrahim
Thank you @pvpkiran! It worked for me! Mockito.when(collection.find(Mockito.any(BasicDBObject.class))).thenReturn(findIterable); Mockito.when(findIterable.iterator()).thenReturn(cursor); Mockito.when(cursor.hasNext()) .thenReturn(true) .thenReturn(false); Mockito.when(cursor.next()).thenReturn(doc); ParticipantDocumentContentInfo participantDocumentContentInfo = personDocumentRepo.getMetaData(docObjectId); Thanks again!Napier
I
5

You return null in collection.find(...) mocked invocation :

FindIterable<Document> findIterable = null;
Mockito.when(collection.find(new Document())).thenReturn(findIterable);

So the mock will return null at runtime. What you need is returning a FindIterable<Document> object that allows to execute the code to test associated to :

for (Document doc : cursorPersonDoc) {
    emplinfo.setEmplFirstName(doc.getString("firstname"));
    emplinfo.setEmplLastName(doc.getString("lastname"));
    break;
}
return emplinfo;

In this way you can assert that the method does what it is designed to : setting the first name and the last name from the retrieved FindIterable<Document>.

You could use the Mockito.mock() method to mock FindIterable<Document> that is an Iterable (whereas the used foreach).
Additionally, to not bother to mock individual methods of Iterator (hasNext(), next()) that could make you test less readable, use a List (that is also an Iterable) to populate the Documents and delegate the behavior of the mocked FindIterable.iterator() to List.iterator().

@Test
public void testGetMetaData() throws Exception {
  ... 
  // add your document instances
  final List<Document> documentsMocked = new ArrayList<>();
  documentsMocked.add(new Document(...));
  documentsMocked.add(new Document(...));

  // mock FindIterable<Document>
   FindIterable<Document> findIterableMocked = (FindIterable<Document>) Mockito.mock(FindIterable.class);

  // mock the behavior of FindIterable.iterator() by delegating to List.iterator()
  when(findIterableMocked.iterator()).thenReturn(documentsMocked.iterator());

  // record a behavior for Collection.find()
  Mockito.when(collection.find(dbObj)).thenReturn(findIterableMocked);

  // execute your method to test
  EmplInfo actualEmplInfo = personDocumentRepo.getMetaData(...);

  // assert that actualEmplInfo has the expected state
  Assert(...);

}

I would add that such a mock will probably not work :

Mockito.when(collection.find(new Document())).thenReturn(findIterable);

Mockito will intercept and replace the behavior of the method invoked on the mock only if the arguments in the recording match (in terms of equals()) with the arguments passed at runtime by the tested method.
At runtime, the argument is build in this way :

BasicDBObject whereClauseCondition = getMetaDataWhereClause(objectId);
EmplInfo emplinfo= new EmplInfo ();
emplinfo.set_id(objectId);

So the argument in the mock recording should be equal to which one defined above.
Note that if equals() is not overriden/overridable for the arguments classes, you have some workarounds such as :

  • passing the object as argument in the method to test (require some refactoring). In this case, Mocked argument and the referenced passed at runtime in the method to test are necessarily equal as these refer the same object

  • matching any object of given type with Mockito.any(Class<T>). Often the simplest way but not the most robust

  • returning an Answer instead of the value to return. That is using Mockito.when(...).then(Answer) instead of Mockito.when(...).thenReturn(valuetoReturn)

Ibrahim answered 22/6, 2018 at 12:39 Comment(4)
Thanks David! We have any option to set FindIterable<Document> findIterable; with some dummy value?Napier
Paah. I ask for clarification, and you put up an answer. Saves me the typing!Parathion
Not a MangoDB specialist but I think that it could be useful base to start.Ibrahim
@Parathion I saw your request. I told myself : he will "kill" me as he will see my answer ! :pIbrahim
S
1

I would like to complete pvpkiran answer with a kind of genric method to mock a DistinctIterable (same as FindIterable).

NB/ in order to avoid sonar warning, I use inner class


    private DistinctIterable<String> mockDistinctIterableString(List<String> resultList) {
        DistinctIterable<String> iterable = mock(DistinctIterableString.class);
        MongoCursor cursor = mock(MongoCursor.class);
        doReturn(cursor)
                .when(iterable).iterator();
        OngoingStubbing<Boolean> whenHasNext = when(cursor.hasNext());
        for (String resultEntry : resultList) {
            whenHasNext = whenHasNext.thenReturn(true);
        }
        whenHasNext.thenReturn(false);
        if (CollectionUtils.isEmpty(resultList)) {
            return iterable;
        }
        OngoingStubbing<Object> whenNext = when(cursor.next());
        for (String resultEntry : resultList) {
            whenNext = whenNext.thenReturn(resultEntry);
        }
        return iterable;
    }

    public interface DistinctIterableString extends com.mongodb.client.DistinctIterable<String> {
    }

use:

        doReturn(mockDistinctIterableString(asList(
                "entry1",
                "entry2",
                "lastEntry"
        )))
        .when(myRepository)
        .myMongoDistinctQuery();
Solly answered 27/1, 2020 at 16:19 Comment(0)
M
0

the @pvpkiran answer is correct when you use a for() directly on the FindIterable

BUT if you use a .forEach method with a lambda, the FindIterable MUST be a spy:

this works:

FindIterable iterable = Mockito.mock(FindIterable.class);
...
for(x : iterable) {

}

this DONT work:

FindIterable iterable = Mockito.mock(FindIterable.class);
...
iterable.forEach(...);

this WORK with forEach (iterable is a spy not a mock):

FindIterable iterable = Mockito.spy(FindIterable.class);
...
iterable.forEach(...);
Metcalf answered 1/8, 2023 at 9:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.