Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock an Elasticsearch Java Client?

Do you know how to propertly mock the Elasticsearch Java Client? Currently to mock the following request in Java:

SearchResponse response = client.prepareSearch(index)
                .setTypes(type)
                .setFrom(0).setSize(MAX_SIZE)
                .execute()
                .actionGet();
SearchHit[] hits = response.getHits().getHits();

I have to mock:

  • client.prepareSearch
  • SearchRequestBuilder:
    • builder.execute
    • builder.setSize
    • builder.setFrom
    • builder.setTypes
  • SearchResponse:
    • action.actionGet
  • SearchResponse:
    • response.getHits
    • searchHits.getHits

So my test looks like:

SearchHit[] hits = ..........;

SearchHits searchHits = mock(SearchHits.class);
when(searchHits.getHits()).thenReturn(hits);

SearchResponse response = mock(SearchResponse.class);
when(response.getHits()).thenReturn(searchHits);

ListenableActionFuture<SearchResponse> action = mock(ListenableActionFuture.class);
when(action.actionGet()).thenReturn(response);

SearchRequestBuilder builder = mock(SearchRequestBuilder.class);
when(builder.setTypes(anyString())).thenReturn(builder);
when(builder.setFrom(anyInt())).thenReturn(builder);
when(builder.setSize(anyInt())).thenReturn(builder);
when(builder.execute()).thenReturn(action);

when(client.prepareSearch(index)).thenReturn(builder);

Ugly... So I would like to known if there is a more "elegant way" to mock this code.

Thanks

like image 418
Thibaut M. Avatar asked Mar 31 '15 12:03

Thibaut M.


1 Answers

I've come across a similar problem when mocking builders, so I thought I'd experiment to see if there is a nicer way.

As Mr Spoon said, it's probably nicer if you can avoid doing do this in the first place as it is not your code and could be assumed to "just work", but I thought I'd give it a go anyway.

I've come up with a (maybe crude) way of doing it by using "default answers" in Mockito. I'm still deciding if I like it or not.

Here's my builder...

public class MyBuilder {

    private StringBuilder my;

    public MyBuilder() {
        my = new StringBuilder();
    }

    public MyBuilder name(String name) {
        my.append("[name=").append(name).append("]");
        return this;
    }

    public MyBuilder age(String age) {
        my.append("[age=").append(age).append("]");
        return this;
    }

    public String create() {
        return my.toString();
    }
}

(Pretty basic right?)

I got my test to look something like this...

// Create a "BuilderMocker" (any better name suggestions welcome!) 
BuilderMocker<MyBuilder> mocker = BuilderMocker.forClass(MyBuilder.class);
// Get the actual mock builder
MyBuilder builder = mocker.build();

// expect this chain of method calls...
mocker.expect().name("[NAME]").age("[AGE]");

// expect this end-of-chain method call...
Mockito.when(builder.create()).thenReturn("[ARGH!]");

Now if I do the following...

System.out.println(builder.name("[NAME]").age("[AGE]").create());

...I expect "[ARGH!]" to be output.

If I altered the last line...

System.out.println(builder.name("[NOT THIS NAME]").age("[AGE]").create());

...then I expect it to break with a NullPointerException.

Here is the actual "BuilderMocker"...

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;

import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public class BuilderMocker<T> {

    private Class<T> clazz;
    private T recorder;
    private T mock;

    // Create a BuilderMocker for the class
    public static <T> BuilderMocker<T> forClass(Class<T> clazz) {
        return new BuilderMocker<T>(clazz);
    }

    private BuilderMocker(Class<T> clazz) {
        this.clazz = clazz;
        this.mock = mock(clazz);
        createRecorder();
    }

    // Sets up the "recorder"
    private void createRecorder() {
        recorder = mock(clazz, withSettings().defaultAnswer(new Answer<Object>() {

            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                // If it is a chained method...
                if (invocation.getMethod().getReturnType().equals(clazz)) {
                    // Set expectation on the "real" mock...
                    when(invocation.getMethod().invoke(mock, invocation.getArguments())).thenReturn(mock);
                    return recorder;
                }
                return null;
            }

        }));
    }

    // Use this to "record" the expected method chain
    public T expect() {
        return recorder;
    }

    // Use this to get the "real" mock...
    public T build() {
        return mock;
    }
}

Not sure if there is a "built-in" way of doing this in Mockito, but this seems to work.

like image 84
BretC Avatar answered Sep 20 '22 17:09

BretC