Jersey Client: Testing external calls

 
Jim and I have been doing a bit of work over the last week which involved calling neo4j’s HA status URI to check whether or not an instance was a master/slave and we’ve been using jersey-client.
 
The code looked roughly like this:
 
 
 
 
 

class Neo4jInstance {
    private Client httpClient;
    private URI hostname;

    public Neo4jInstance(Client httpClient, URI hostname) {
        this.httpClient = httpClient;
        this.hostname = hostname;
    }

    public Boolean isSlave() {
        String slaveURI = hostname.toString() + ":7474/db/manage/server/ha/slave";
        ClientResponse response = httpClient.resource(slaveURI).accept(TEXT_PLAIN).get(ClientResponse.class);
        return Boolean.parseBoolean(response.getEntity(String.class));
    }
}

While writing some tests against this code we wanted to stub out the actual calls to the HA slave URI so we could simulate both conditions and a brief search suggested that mockito was the way to go.

We ended up with a test that looked like this:

@Test
public void shouldIndicateInstanceIsSlave() {
    Client client = mock( Client.class );
    WebResource webResource = mock( WebResource.class );
    WebResource.Builder builder = mock( WebResource.Builder.class );

    ClientResponse clientResponse = mock( ClientResponse.class );
    when( builder.get( ClientResponse.class ) ).thenReturn( clientResponse );
    when( clientResponse.getEntity( String.class ) ).thenReturn( "true" );
    when( webResource.accept( anyString() ) ).thenReturn( builder );
    when( client.resource( anyString() ) ).thenReturn( webResource );

    Boolean isSlave = new Neo4jInstance(client, URI.create("http://localhost")).isSlave();

    assertTrue(isSlave);
}

which is pretty gnarly but does the job.

I thought there must be a better way so I continued searching and eventually came across this post on the mailing list which suggested creating a custom ClientHandler and stubbing out requests/responses there.

I had a go at doing that and wrapped it with a little DSL that only covers our very specific use case:

private static ClientBuilder client() {
    return new ClientBuilder();
}

static class ClientBuilder {
    private String uri;
    private int statusCode;
    private String content;

    public ClientBuilder requestFor(String uri) {
        this.uri = uri;
        return this;
    }

    public ClientBuilder returns(int statusCode) {
        this.statusCode = statusCode;
        return this;
    }

    public Client create() {
        return new Client() {
            public ClientResponse handle(ClientRequest request) throws ClientHandlerException {
                if (request.getURI().toString().equals(uri)) {
                    InBoundHeaders headers = new InBoundHeaders();
                    headers.put("Content-Type", asList("text/plain"));
                    return createDummyResponse(headers);
                }
                throw new RuntimeException("No stub defined for " + request.getURI());
            }
        };
    }

    private ClientResponse createDummyResponse(InBoundHeaders headers) {
        return new ClientResponse(statusCode, headers, new ByteArrayInputStream(content.getBytes()), messageBodyWorkers());
    }

    private MessageBodyWorkers messageBodyWorkers() {
        return new MessageBodyWorkers() {
            public Map<MediaType, List<MessageBodyReader>> getReaders(MediaType mediaType) {
                return null;
            }

            public Map<MediaType, List<MessageBodyWriter>> getWriters(MediaType mediaType) {
                return null;
            }

            public String readersToString(Map<MediaType, List<MessageBodyReader>> mediaTypeListMap) {
                return null;
            }

            public String writersToString(Map<MediaType, List<MessageBodyWriter>> mediaTypeListMap) {
                return null;
            }

            public <T> MessageBodyReader<T> getMessageBodyReader(Class<T> tClass, Type type, Annotation[] annotations, MediaType mediaType) {
                return (MessageBodyReader<T>) new StringProvider();
            }

            public <T> MessageBodyWriter<T> getMessageBodyWriter(Class<T> tClass, Type type, Annotation[] annotations, MediaType mediaType) {
                return null;
            }

            public <T> List<MediaType> getMessageBodyWriterMediaTypes(Class<T> tClass, Type type, Annotation[] annotations) {
                return null;
            }

            public <T> MediaType getMessageBodyWriterMediaType(Class<T> tClass, Type type, Annotation[] annotations, List<MediaType> mediaTypes) {
                return null;
            }
        };
    }

    public ClientBuilder content(String content) {
        this.content = content;
        return this;
    }
}

If we change our test to use this code it now looks like this:

@Test
public void shouldIndicateInstanceIsSlave() {
    Client client = client().requestFor("http://localhost:7474/db/manage/server/ha/slave").
                             returns(200).
                             content("true").
                             create();
    Boolean isSlave = new Neo4jInstance(client, URI.create("http://localhost")).isSlave();
    assertTrue(isSlave);
}

Is there a better way?

In Ruby I’ve used WebMock to achieve this and Ashok pointed me towards WebStub which looks nice except I’d need to pass in the hostname + port rather than constructing that in the code.

 

Reference: Jersey Client: Testing external calls from our JCG partner Mark Needham at the Mark Needham Blog blog.
Related Whitepaper:

Functional Programming in Java: Harnessing the Power of Java 8 Lambda Expressions

Get ready to program in a whole new way!

Functional Programming in Java will help you quickly get on top of the new, essential Java 8 language features and the functional style that will change and improve your code. This short, targeted book will help you make the paradigm shift from the old imperative way to a less error-prone, more elegant, and concise coding style that’s also a breeze to parallelize. You’ll explore the syntax and semantics of lambda expressions, method and constructor references, and functional interfaces. You’ll design and write applications better using the new standards in Java 8 and the JDK.

Get it Now!  

2 Responses to "Jersey Client: Testing external calls"

  1. Hi,

    If you make use of the Jersey class TerminatingClientHandler you can simplify this code quite a bit, here in the test class that I use in the wadl.java.net project:

    https://java.net/projects/wadl/sources/svn/content/trunk/wadl/wadl-maven-plugin/src/test/java/org/jvnet/ws/wadl/maven/Wadl2JavaMojoTest.java?rev=389

    And the same code, obviously slightly different, when using JAX-RS 2.0:

    https://java.net/projects/wadl/sources/svn/content/trunk/wadl/wadl-maven-plugin/src/test/java/org/jvnet/ws/wadl/maven/Wadl2JavaMojoJAXRS20Test.java?rev=389

    Hope this is helpful,

    Gerard

  2. G.H. says:

    The second approach aside: How did you get the first one to even run? When I try mocking WebResource.Builder as you do up there, I get the following:

    org.mockito.exceptions.base.MockitoException:
    Cannot mock/spy class com.sun.jersey.api.client.WebResource$Builder
    Mockito cannot mock/spy following:
    – final classes
    – anonymous classes
    – primitive types

    as WebResource.Builder is declared final.

Leave a Reply


2 − = zero



Java Code Geeks and all content copyright © 2010-2014, Exelixis Media Ltd | Terms of Use | Privacy Policy
All trademarks and registered trademarks appearing on Java Code Geeks are the property of their respective owners.
Java is a trademark or registered trademark of Oracle Corporation in the United States and other countries.
Java Code Geeks is not connected to Oracle Corporation and is not sponsored by Oracle Corporation.

Sign up for our Newsletter

20,709 insiders are already enjoying weekly updates and complimentary whitepapers! Join them now to gain exclusive access to the latest news in the Java world, as well as insights about Android, Scala, Groovy and other related technologies.

As an extra bonus, by joining you will get our brand new e-books, published by Java Code Geeks and their JCG partners for your reading pleasure! Enter your info and stay on top of things,

  • Fresh trends
  • Cases and examples
  • Research and insights
  • Two complimentary e-books