Mocking fluent interfaces with Mockito
October 26, 2017
In last years I see more and more libraries providing APIs in the form of fluent interfaces. If you are not sure what a fluent interface is, I recommend reading Martin Fowler’s article from 2005, where he coined this term.
In software engineering, a fluent interface is a method for constructing object oriented APIs, where the readability of the source code is close to that of ordinary written prose.
The example of such an interface we can find in
MongoOperations interface from Spring Data MongoDB:
mongoOperations.query(SWCharacter.class) .inCollection("star-wars") .as(Jedi.class) .matching(query(where("name").is("luke"))) .one();
Recently I came across a short discussion on Twitter on how painful is mocking fluent interfaces:
Any pointers towards testing/mocking fluent apis? Can't agree more them being PITA.— Oyku Gencay (@oyku) October 11, 2017
Let’s have a look how we can approach this problem with the most popular mocking library for Java - Mockito. Typically, mocking the above code sample would result in the following:
FluentMongoOperations mongoOperations = mock(FluentMongoOperations.class); ExecutableFind<SWCharacter> find = mock(ExecutableFind.class); FindWithProjection<SWCharacter> findWithProjection = mock(FindWithProjection.class); FindWithQuery<Jedi> findWithQuery = mock(FindWithQuery.class); TerminatingFind<Jedi> terminatingFind = mock(TerminatingFind.class); when(mongoOperations.query(SWCharacter.class)).thenReturn(find); when(find.inCollection("star-wars")).thenReturn(findWithProjection); when(findWithProjection.as(Jedi.class)).thenReturn(findWithQuery); when(findWithQuery.matching(query(where("name").is("luke")))).thenReturn(terminatingFind); when(terminatingFind.one()).thenReturn(Optional.of(jedi));
While many things in software engineering are a matter of taste, I am sure that it would be difficult to find anyone who would say that this code qualifies as “clean”.
Fortunately, there is a better approach. Mockito allows setting an answer mode when mocks are created.
- CALLS_REAL_METHODS - An answer that calls the real methods (used for partial mocks).
- RETURNS_DEEP_STUBS - An answer that returns deep stubs (not mocks).
- RETURNS_DEFAULTS - The default configured answer of every mock.
- RETURNS_MOCKS - An answer that returns mocks (not stubs).
- RETURNS_SELF - An answer that tries to return itself.
- RETURNS_SMART_NULLS - An answer that returns smart-nulls.
Mocking fluent interfaces works the best with Answers#RETURN_DEEP_STUBS. Once we declare mock to return deep stubs:
FluentMongoOperations mongoOperations = mock(FluentMongoOperations.class, Mockito.RETURNS_DEEP_STUBS);
- or with annotations:
@Mock(answer = Answers.RETURNS_DEEP_STUBS) FluentMongoOperations mongoOperations;
Code from the beginning of the post gets simplified to:
when(mongoOperations.query(SWCharacter.class) .inCollection("star-wars") .as(Jedi.class) .matching(query(where("name").is("luke"))) .one()).thenReturn(Optional.of(jedi));