Skip to content

The Client

psamsotha edited this page Jun 26, 2016 · 10 revisions

This page will explain how to use the main component of this project, which is the client API, and how it works under the hood. It will also go into some explaining about the reasoning I chose to go this route, and some of the pros and cons and work-arounds.

How to Use

To first get started, you will need to add the dependency. Because the project is dependent on Jersey client and a couple of other Jersey dependencies, it is recommended that you override these transitive dependencies to match the version of Jersey you are using for your main project. This will ensure that there are no version conflicts.

Dependencies

For Maven users, you can add add the following

<dependencies>
    <dependency>
        <groupId>io.github.restdocsext</groupId>
        <artifactId>restdocsext-jersey</artifactId>
        <version>${restdocsext.jersey.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.core</groupId>
        <artifactId>jersey-client</artifactId>
        <version>${your.jersey.version}</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.media</groupId>
        <artifactId>jersey-media-json-jackson</artifactId>
        <version>${your.jersey.version}</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.media</groupId>
        <artifactId>jersey-media-multipart</artifactId>
        <version>${your.jersey.version}</version>
    </dependency>
</dependencies>

The ${your.jersey.version} should be the version of Jersey you are using for your project. The ${restdocsext.jersey.version} is the RESTDocsEXT Jersey version. See the project Homepage for the latest version. It is recommended you only use this project's dependency in a test scope, for reasons that will be mentioned later on in Under the Hood.

For Gradle users, this project's artifacts are hosted on Bintray, jcenter, and Maven Central. So you can use any of the three repositories shown below

repositories {
    maven {
        url 'https://dl.bintray.com/psamsotha/RESTDocsEXT'
    }
    jcenter()
    mavenCentral()
}

dependencies {
    testCompile "io.github.restdocsext:restdocsext-jersey:$restdocsJerseyVersion"
    compile "org.glassfish.jersey.core:jersey-client:$yourJerseyVersion"
    compile "org.glassfish.jersey.media:jersey-media-multipart:$yourJerseyVersion"
    compile "org.glassfish.jersey.media:jersey-media-json-jackson:$yourJerseyVersion"
}

For the latest version, please see the project homepage.

Using the Client

If you have used the Jersey Client API before, you probably have never directly used the Jersey client APIs, but accessed it using standard JAX-RS client APIs. As Jersey is an implementation of the JAX-RS specification, using the standard APIs would actually result in transparently using Jersey's implementation. For example to get a Client instance, using the standard JAX-RS APIs, you most likely have done this before

Client client = ClientBuilder.newClient();

To make a request, you would use the client to do something like

Client client = ClientBuilder.newClient();
WebTarget target = client.target(url);
Response response = target.path("/path")
        .queryParam("param", "value")
        .request()
        .header("X-Header", "value")
        .get();

To use the client with RESTDocsEXT Jersey, nothing changes from this. You use the client in the exact same way. The only difference is that when you actually want to perform a documentation operation, you just register the necessary components with the WebTarget

Response response = client.target(uri)
        .register(document("get-simple"))
        .register(documentationConfiguration(this.documentation))
        .path("/path")
        .queryParam("param", "value")
        .request()
        .header("X-Header", "value")
        .get();

The document and documentationConfiguration methods will be explained below in the examples.

Jersey Test Framework

When using Jersey Test Framework, you generally will not need to explicitly create the client. For example your test might look something like

public class ResourceTest extends JerseyTest {
    ...
    @Test
    public void testIt() {
        Response response = target("/path").request().get();
    }
}

Here, the call to target actually returns a WebTarget instance that is created in the the background using the came Client/WebTarget creation syntax as shown above. Again, using RESTDocsEXT Jersey, nothing changes. You will continue to use this same exact syntax, with no change. Below are complete examples (with some explanation) using both the more commonly used JUnit support for the framework, and also the TestNG support for the framework.

The core test framework supports both JUnit and TestNG, so for which ever one you decide the use, the test framework dependency will be the same

Maven

<dependency>
    <groupId>org.glassfish.jersey.test-framework.providers<groupId>
    <artifactId>jersey-test-framework-provider-inmemory</artifactId>
    <version>${your.jersey.version}</version>
    <scope>test</scope>
</dependency>

Gradle

// should be all on one line
testCompile "org.glassfish.jersey.test-framework.providers
             :jersey-test-framework-provider-inmemory
             :$jerseyVersion"

Please have a look at the Jersey documentation for other supported containers, aside from the in-memory one used here. One thing to note about the in-memory provider, is that it does not support using Servlet APIs like HttpServletRequest and ServletContext. If you aren't using any of the Servlet APIs in your project, then the in-memory provider is a good fit, as it is faster than running a real server using the other providers.

JUnit Support

Below is an example using the JUnit support.

// Other imports excluded for brevity
import org.springframework.restdocs.JUnitRestDocumentation;

import static io.github.restdocsext.jersey.JerseyRestDocumentation.document;
import static io.github.restdocsext.jersey.JerseyRestDocumentation.documentationConfiguration;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders;

public class JUnitSimpleDocumentation extends JerseyTest {

    @Rule
    public JUnitRestDocumentation documentation  // < 1 >
            = new JUnitRestDocumentation("build/generated-snippets");

    @Path("test")
    public static class TestResource {
        @GET
        public String getSimple() {
            return "SimpleTesting";
        }
    }

    @Override
    public ResourceConfig configure() {
        return new ResourceConfig(TestResource.class);
    }

    @Test
    public void getSimple() {
        final Response response = target("test")
                .register(documentationConfiguration(this.documentation))  // < 2 >
                .register(document("get-simple-junit",  // < 3 >
                        preprocessRequest(removeHeaders("User-Agent"))))  // < 4 >
                .request()
                .get();
        assertThat(response.getStatus(), is(200));
        assertThat(response.readEntity(String.class), is("SimpleTesting"));
    }
}
  1. The is the JUnit rule that is required for Spring REST Docs to store context information about the current documentation operation. The value passed to the JUnitRestDocumentation constructor is the directory where the generated snippets should be stored. In a Gradle project, you generally want this in the build directory, whereas in a Maven project, you will probably want it in the target directory.

  2. This is the configuration of the documentation.

  3. The component returned from the static document method is the component that handles the actual documentation. There are many things that can be configure within the context of this method call.

  4. Here we are setting a pre-processor, telling Spring REST Docs to exclude the User-Agent header from all the documentation snippets. Jersey Test Framework seems to add this header, so we want it removed.

TestNG Support

For TestNG support, you should also remember to add the TestNG dependency. Though the test framework supports TestNG, it doesn't include it as a runtime dependency, so we should add it ourselves.

Maven

<dependency>
    <groupId>org.testng</groupId>
    <artifactId>testng</artifactId>
    <version>6.8</version>
    <scope>test</scope>
</dependency>

Gradle

dependencies {
    testCompile "org.testng:testng:6.8"
}

For Gradle users you should also add the following to the build file in order for TestNG to actually run the tests.

tests {
    useTestNG()
}

I forgot to add this while testing this example and spent a pit of time trying to figure out what the heck the problem was. Maybe you can guess I am not a TestNG user :-)

import java.lang.reflect.Method;
// some imports excluded for brevity
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import static io.github.restdocsext.jersey.JerseyRestDocumentation.document;
import static io.github.restdocsext.jersey.JerseyRestDocumentation.documentationConfiguration;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders;

public class TestNGSimpleDocumentation extends JerseyTestNg.ContainerPerMethodTest {
    
    private final ManualRestDocumentation restDocumentation =  // < 1 >
		new ManualRestDocumentation("build/generated-snippets");
    
    @BeforeMethod
    public void setUp(Method method) {
        this.restDocumentation.beforeTest(getClass(), method.getName()); // < 2 >
    }
    
    @AfterMethod
    public void takeDown() {
        this.restDocumentation.afterTest();  // < 3 >
    }
    
    @Path("test")
    public static class TestResource {
        @GET
        public String getSimple() {
            return "SimpleTesting";
        }
    }

    @Override
    public ResourceConfig configure() {
        return new ResourceConfig(TestResource.class);
    }

    @Test
    public void getSimple() {
        final Response response = target("test")
                .register(documentationConfiguration(this.restDocumentation))  // < 4 >
                .register(document("get-simple-testng",  // < 5 >
                        preprocessRequest(removeHeaders("User-Agent"))))  // < 6 >
                .request()
                .get();
        assertThat(response.getStatus(), is(200));
        assertThat(response.readEntity(String.class), is("SimpleTesting"));
    }
}
  1. Unlike the JUnit example this example uses the ManualRestDocumentation. The reason for this is that JUnit has a Rule feature which is similar to how AOP works. Rules will transparently handle some tasks before and after test methods (or even test classes, given it is a class level Rule). Under the hood, the JUnitRestDocumentation actually uses the ManualRestDocumentation, and makes the same calls as seen above in the @BeforeMethod and @AfterMethod method. Since TestNG does not have this Rule like feature1, we need to manually invoke these required operations.

    The argument to the ManualRestDocumentation constructor is the location we want Spring REST Docs to output the snippets to.

  2. This method on the ManualRestDocumentation should be called before each test.

  3. The method on the ManualRestDocumentation should be called after each test.

  4. This is the configuration of the documentation.

  5. The component returned from the static document method is the component that handles the actual documentation. There are many things that can be configured within the context of this method call.

  6. Here we are setting a pre-processor telling Spring REST Docs to exclude the User-Agent header from all the documentation snippets. Jersey Test Framework seems to add this header, so we want it removed.

For more information about TestNG with Jersey Test Framework, please see the Jersey documentation. For more information about TestNG support for Spring REST Docs, please see the Spring REST Docs documentation

1 - Or maybe it does. Like I said, I am not much of a TestNG user. But if it does have some feature like this, I'm guessing it would not integrate seamlessly enough with Spring REST Docs, and that's why it is implemented this way.

Test Output

Both the JUnit and TestNG examples above should produce the same output. These are the default snippets that are produced without configuring any other snippets. Whatever directory you configured in the XxxRestDocumentation in the test class, this is the directory the snippets will appear in.

If you were running the JUnit example, the snippets should appear in the get-simple-junit sub-directory. If you ran the TestNG example, the snippets should appear in the get-simple-testng sub-directory. These directories correspond to the value in the document("get-simple-xxx") method.

curl-request.adoc

[source,bash]
----
$ curl 'http://localhost:9998/test' -i
----

http-request.adoc

[source,http,options="nowrap"]
----
GET /test HTTP/1.1
Host: localhost

----

http-response.adoc

[source,http,options="nowrap"]
----
HTTP/1.1 200 OK
Content-Length: 13
Date: Wed, 15 Jun 2016 03:48:58 GMT
Content-Type: text/html

SimpleTesting
----

httpie-request.adoc

[source,bash]
----
$ http GET 'http://localhost:9998/test'
----

Adding More Snippets

With the static document factory method, we can pass extra snippets to create, rather than just the default snippets. The following are the available overloaded document methods.

document(String identifier, Snippet... snippets)

document(String identifier,
            OperationRequestPreprocessor requestPreprocessor, Snippet... snippets)

document(String identifier,
            OperationResponsePreprocessor responsePreprocessor, Snippet... snippets)

document(String identifier,
            OperationRequestPreprocessor requestPreprocessor,
            OperationResponsePreprocessor responsePreprocessor, Snippet... snippets)

The examples above used the second of these overloads listed. It simply passed the string identifier (this will be the directory output for the snippets), and it used a request pre-processor (which is a Spring REST Docs core API) to take a certain header out of the snippets.

As you can see in all the overloads, there is a varargs parameter for Snippet instances. So for example if we wanted to document some path parameters, we could do something like

final Response response = target("test")
        .path("{testId}")
        .resolveTemplate("testId", "1")
        .register(documentationConfiguration(this.documentation)) 
        .register(document("get-simple-junit",
                preprocessRequest(removeHeaders("User-Agent")),
                pathParameters(
                        parameterWithName("testId").description("The test id"))))  
        .request()
        .get();

Here the pathParameters is a factory method in the Spring REST Docs core RequestDocumentation class, which returns a Snippet.

For more information on using the Spring REST Docs core APIs, please see the Spring REST Docs Reference Documentation.


Under the Hood

As mentioned previously, you can obtain a Client instance, by doing the following

Client client = ClientBuilder.newClient();

If you are using the jersey-client, the result from this code will an instance of JerseyClient, which implements the Client interface.

It works the same way with RESTDocsEXT Jersey. If you have RESTDocsEXT Jersey on the classpath, the previous code will not result in a JerseyClient, but instead a RestdocsClient. This all happens transparently under the hood, just using the standard JAX-RS client APIs.

To make any use of the Client, you most likely have also worked with the WebTarget, which is also a standard JAX-RS client API. For instance you may have seen this

Client client = ClientBuilder.newClient();
WebTarget target = client.target(url);

Normally using the Jersey client, the resulting WebTarget instance would actually be an instance of JerseyWebTarget. Most people don't know this, and rightly so. There usually is never a need to have to use the JerseyWebTarget implementation directly.

This same rule of transparency applies when using RESTDocsEXT Jersey. If you have RESTDocsEXT Jersey on the classpath, the above WebTarget would actually be a RestdocsWebTarget instance.

So how does it all work?

If you look inside the restdocsext-jersey jar, inside the /META-INF/services directory, you should see a file named javax.ws.rs.client.ClientBuilder. The contents of this file being the name of an implementation of the ClientBuilder class. In this case, it is io.github.restdocsext.jersey.client.RestdocsClientBuilder.

The ClientBuilder class is a core JAX-RS API, but how it works under the hood is that it looks for the specific previously mentioned file on the classpath. If it finds the file, it will try to instantiate the mentioned class in the file. The result will be an instance of that class.

But what about the jersey-client jar? Does it not also have this file? Actually it doesn't. If you look at the source code for the ClientBuilder, you will actually see it explicitly mentions the JerseyClientBuilder as the default, if no file is found. So if no file is found, it will try to instantiate the instance of the JerseyClientBuilder.

The RestdocsClientBuilder is used to create this project's specific RestdocsClient (a Client implementation). The RestdocsClient wraps an instance of JerseyClient, delegating all the calls to the JerseyClient. The reason for this is so we can do some pre-processing when methods are called, before we delegate the call to the JerseyClient. I guess you can call this a Decorator Pattern.


Reasoning, Pros & Cons, Work-Arounds

As mentioned in the previous section, just having RESTDocsEXT Jersey on the classpath, is enough to use its specific client, just using the standard JAX-RS client "entry point" API. But this makes it so that the normal JerseyClient is not used.

While exploring different possible implementations for this project, I explored the possibility just extending the Jersey Test Framework, but this would tie it too tightly with the framework, and introduce the new APIs to the framework. I thought this current transparent implementation using just the client was more elegant, and I think the benefits out-weight the few drawbacks. All of the drawbacks come from using the RestdocsClient for tests that are not meant to be documentation tests. There are simple work-arounds for this.

If you want to use the JerseyClient, you can always just use the Jersey specific ClientBuilder implementation

Client client = JerseyClientBuilder.createClient();

This will give you the JerseyClient instance. With JerseyTest 2.17+, you can override the Client being used. There is a setClient(Client) method on JerseyTest. Or you can override Client getClient(). Either way would work

public class SomeTest extends JerseyTest {
    @Override
    public Client getClient() {
        return JerseyClientBuilder.createClient();
    }
    // OR
    @Override
    public ResourceConfig configure() {
        setClient(JerseyClientBuilder.createClient());
    }
}

Prior to Jersey 2.17, where these two methods aren't public, you would need to just use your own client explicitly for the tests, instead of calling the target method on the JerseyTest. The default port for JerseyTest is 9998. It's not really that obvious. So when when you create the URI to pass to client.targer(url), keep that in mind. It's also possible to override the default port by overriding the getBaseUri

public class SomeTest extends JerseyTest {
    @Test
    public void test() {
        Client client = JerseyClientBuilder.createClient();
        Response response = client.target(url).request().get();
    }
    @Override
    public URI getBaseUri() {
        return URI.create("http://localhost:8080/");
    }
}

One problem with this approach is that you can't use the in-memory provider. You need to use a provider that starts an actual server and binds to the network.

If you are using the DropWizard test Rules, you will pretty much need to do the same thing as mention above: just create the client yourself, instead of use the client obtained from the Rule. DropWizard test Rules actually use Jersey Test Framework under the hood. So the default ports should be the same as mentioned previously.

If in the event you decide just to use the RestdocsClient for your non-documentation tests, here are a few things to consider.

  1. When you call certain methods on the client, internal properties are stored. This really should not have any affect though on usage of the client.

  2. A couple of filters and interceptors are added under the hood. For the most part, I haven't found any case where this affects any tests, but there is a configuration property to disable these filters. Just set the DocumentationProperties.DISABLE_INTERCEPTORS property on the client to true

     Client client ...
     client.property(DocumentationProperties.DISABLE_INTERCEPTORS, true);
    

Some Important Usage Notes

Documenting Path Parameters

Spring REST Docs allows for documenting path parameters. For example if your endpoint supports a template of /api/pets/{petId}, you can document the petId path parameter. To get support for this, you should build your requests with the templates, and then replace them. Under the hood the template will be extracted.

    target("/api/pets/{petId}")
        .resolveTemplate("petId", "1")
        .request().get();
    // or
    target("/api/pets")
        .path("{petId}")
        .resolveTemplate("petId", "1")
        .request().get();

Documenting MultiPart

If you are documenting a multi part endpoint, it's likely that the request will involve sending a file, most likely a binary file. When the snippet documents the request, you will end up seeing a bunch of garbled data in the snippet. For this, there is an request pre-processor that you can use to add a placeholder for the binary content, maybe something like <<binary data>>. To use it, you can do something like

document("multipart-request",
        preprocessRequest(
                binaryParts().field("file", "<<image-data>>")))

The binaryParts method is a static factory method in the JerseyPreprocessors class. You can chain multiple field calls if you have multiple fields you want to add placeholder content for. The field method is overloaded, with one or two arguments. If you just call the one argument method, the placeholder content will the default placeholder <<binary data>>. Calling the two-argument method, allows you to specify a different placeholder.

document("multipart-request",
        preprocessRequest(
                binaryParts()
                    .field("file")
                    .field("image", "<<image data>>")))