Featured imaged

In previous post I discussed about Spring REST docs and I mentioned that I’m planning to give it a try. I was so enthusiastic when playing with Spring REST docs that I forgot that we are using Jersey. At first I was disappointed but then I started to look for a solution. The first hope come from Stack Overflow where I found that it is possible to use Spring Restdocs with Jersey application. I dug more and I updated my greeting service to use Jersey.

The first change was in pom.xml that was updated from:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.restdocs</groupId>
        <artifactId>spring-restdocs-mockmvc</artifactId>
    </dependency>
    <dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

to

<properties>
    <jersey.test.framework.version>1.19</jersey.test.framework.version>
    <jersey.test.framework.provider.grizzly2.version>2.23.1</jersey.test.framework.provider.grizzly2.version>
    <spring.restdocs.restassured.version>1.1.0.RELEASE</spring.restdocs.restassured.version>
    <spring.restdocs.core.version>1.1.0.RELEASE</spring.restdocs.core.version>
    <system.rules.version>1.16.0</system.rules.version>
    <asciidoctor.maven.plugin.version>1.5.2</asciidoctor.maven.plugin.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jersey</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.sun.jersey.jersey-test-framework</groupId>
        <artifactId>jersey-test-framework-core</artifactId>
        <version>${jersey.test.framework.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.test-framework.providers</groupId>
        <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
        <version>${jersey.test.framework.provider.grizzly2.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.restdocs</groupId>
        <artifactId>spring-restdocs-restassured</artifactId>
        <version>${spring.restdocs.restassured.version}</version>
        <scope>test</scope>
    </dependency>
    <!-- It fails with spring-restdocs-core from spring-restdocs-restassured -->
    <dependency>
        <groupId>org.springframework.restdocs</groupId>
        <artifactId>spring-restdocs-core</artifactId>
        <version>${spring.restdocs.core.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.github.stefanbirkner</groupId>
        <artifactId>system-rules</artifactId>
        <version>${system.rules.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>

I had to explicitly add org.springframework.restdocs:spring-restdocs-core with version 1.1.0.RELEASE because the transitive included one with version 1.0.1.RELEASE caused the following problems:

  • org.springframework.restdocs.RestDocumentation does not implement org.springframework.restdocs.RestDocumentationContextProvider
  • org.springframework.restdocs.restassured.RestAssuredRestDocumentation#documentationConfiguration() does not compile when an org.springframework.restdocs.RestDocumentation instance is sent as its parameter:
[ERROR] /media/data/Work/espressoprogrammer-code/greeting-service-jersey/src/test/java/com/espressoprogrammer/hello/GreetingServiceTest.java:[59,56]
incompatible types: org.springframework.restdocs.RestDocumentation cannot be converted to org.springframework.restdocs.RestDocumentationContextProvider

After I explicitly added org.springframework.restdocs:spring-restdocs-core with version 1.1.0.RELEASE I was able to compile.

Code was updated also like bellow.

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

changed to:

@SpringBootApplication
public class Application extends SpringBootServletInitializer {

    public static void main(String[] args) {
        new Application()
                .configure(new SpringApplicationBuilder(Application.class))
                .run(args);
    }

}
@RestController
@RequestMapping("/greeting")
public class GreetingController {

    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();

    @RequestMapping(method = RequestMethod.GET)
    public Greeting greeting(@RequestParam(value="name", defaultValue="World") String name) {
        return new Greeting(counter.incrementAndGet(), String.format(template, name));
    }

}

changed to:

@Component
@Path("/greeting")
public class GreetingService {

    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Greeting greeting(@QueryParam("name") @DefaultValue("World") String name) {
        return new Greeting(counter.incrementAndGet(), String.format(template, name));
    }

}
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class GreetingControllerTest {

    @Rule
    public RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets");

    @Autowired
    private WebApplicationContext context;

    private MockMvc mockMvc;

    @Before
    public void setUp(){
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
                .apply(documentationConfiguration(this.restDocumentation))
                .build();
    }

    @Test
    public void greetingGetWithProvidedContent() throws Exception {

        this.mockMvc.perform(get("/greeting"). param("name", "Everybody"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
                .andExpect(jsonPath("$.content", is("Hello, Everybody!")))
                .andDo(document("{class-name}/{method-name}",
                        requestParameters(parameterWithName("name").description("Greeting's target")),
                        responseFields(fieldWithPath("id").description("Greeting's generated id"),
                                fieldWithPath("content").description("Greeting's content"),
                                fieldWithPath("optionalContent").description("Greeting's optional content").type(JsonFieldType.STRING).optional()
                )));

    }

    @Test
    public void greetingGetWithDefaultContent() throws Exception {

        this.mockMvc.perform(get("/greeting"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
                .andExpect(jsonPath("$.content", is("Hello, World!")))
                .andDo(document("{class-name}/{method-name}",
                        responseFields(fieldWithPath("id").ignored(),
                                fieldWithPath("content").description("When name is not provided, this field contains the default value")
                )));

    }

}

changed to:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class GreetingServiceTest extends JerseyTest {
    private static final Integer JERSEY_CONTAINER_PORT = 8080;

    @Rule
    public final ProvideSystemProperty myPropertyHasMyValue = new ProvideSystemProperty("jersey.config.test.container.port", JERSEY_CONTAINER_PORT.toString());

    @Rule
    public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets");

    @Test
    public void greetingGetWithProvidedContent() throws Exception {
        given()
                .port(JERSEY_CONTAINER_PORT)
                .filter(documentationConfiguration(this.restDocumentation))
                .filter(document("{class-name}/{method-name}",
                        requestParameters(parameterWithName("name").description("Greeting's target")),
                        responseFields(fieldWithPath("id").description("Greeting's generated id"),
                                fieldWithPath("content").description("Greeting's content"),
                                fieldWithPath("optionalContent").description("Greeting's optional content").type(JsonFieldType.STRING).optional()
                        )))
                .accept(MediaType.APPLICATION_JSON.toString())
                .get("/greeting?name={id}", "Everybody")
                .then()
                .statusCode(HttpStatus.OK.value())
                .assertThat().contentType(equalTo(MediaType.APPLICATION_JSON.toString()))
                .assertThat().body("content", equalTo("Hello, Everybody!"));
    }

    @Test
    public void greetingGetWithDefaultContent() throws Exception {
        given()
                .port(JERSEY_CONTAINER_PORT)
                .filter(documentationConfiguration(this.restDocumentation))
                .filter(document("{class-name}/{method-name}",
                        responseFields(fieldWithPath("id").ignored(),
                                fieldWithPath("content").description("When name is not provided, this field contains the default value"))))
                .accept(MediaType.APPLICATION_JSON.toString())
                .get("/greeting")
                .then()
                .statusCode(HttpStatus.OK.value())
                .assertThat().contentType(equalTo(MediaType.APPLICATION_JSON.toString()))
                .assertThat().body("content", equalTo("Hello, World!"));
    }

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

}

I had to use org.springframework.restdocs.JUnitRestDocumentation because org.springframework.restdocs.RestDocumentation is deprecated in org.springframework.restdocs:spring-restdocs-core with version 1.1.0.RELEASE.

= Greeting REST Service API Guide
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:

= Resources

== Greeting REST Service

The Greeting provides the entry point into the service.

=== Accessing the greeting GET with provided content

==== Request structure

include::{snippets}/greeting-controller-test/greeting-get-with-provided-content/http-request.adoc[]

==== Request parameters

include::{snippets}/greeting-controller-test/greeting-get-with-provided-content/request-parameters.adoc[]

==== Response fields

include::{snippets}/greeting-controller-test/greeting-get-with-provided-content/response-fields.adoc[]

==== Example response

include::{snippets}/greeting-controller-test/greeting-get-with-provided-content/http-response.adoc[]

==== CURL request

include::{snippets}/greeting-controller-test/greeting-get-with-provided-content/curl-request.adoc[]

=== Accessing the greeting GET with default content

==== Request structure

include::{snippets}/greeting-controller-test/greeting-get-with-default-content/http-request.adoc[]

==== Response fields

include::{snippets}/greeting-controller-test/greeting-get-with-default-content/response-fields.adoc[]

==== Example response

include::{snippets}/greeting-controller-test/greeting-get-with-default-content/http-response.adoc[]

==== CURL request

include::{snippets}/greeting-controller-test/greeting-get-with-default-content/curl-request.adoc[]

changed to:

= Greeting REST Service API Guide
Jersey implementation;
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:

= Resources

== Greeting REST Service

The Greeting provides the entry point into the service.

=== Accessing the greeting GET with provided content

==== Request structure

include::{snippets}/greeting-service-test/greeting-get-with-provided-content/http-request.adoc[]

==== Request parameters

include::{snippets}/greeting-service-test/greeting-get-with-provided-content/request-parameters.adoc[]

==== Response fields

include::{snippets}/greeting-service-test/greeting-get-with-provided-content/response-fields.adoc[]

==== Example response

include::{snippets}/greeting-service-test/greeting-get-with-provided-content/http-response.adoc[]

==== CURL request

include::{snippets}/greeting-service-test/greeting-get-with-provided-content/curl-request.adoc[]

=== Accessing the greeting GET with default content

==== Request structure

include::{snippets}/greeting-service-test/greeting-get-with-default-content/http-request.adoc[]

==== Response fields

include::{snippets}/greeting-service-test/greeting-get-with-default-content/response-fields.adoc[]

==== Example response

include::{snippets}/greeting-service-test/greeting-get-with-default-content/http-response.adoc[]

==== CURL request

include::{snippets}/greeting-service-test/greeting-get-with-default-content/curl-request.adoc[]

The only major change here was that include::{snippets}/greeting-controller-test changed to include::{snippets}/greeting-service-test because I renamed the test class.

I had to add:

@Component
public class JerseyConfig extends ResourceConfig {

    public JerseyConfig() {
        register(GreetingService.class);
    }

}

and an empty applicationContext.xml in src/test/resources otherwise Jersey test fails:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans   http://www.springframework.org/schema/beans/spring-beans.xsd">

</beans>

After running mvn clean package generated documentation can be found in target/generated-docs folder:

generated documentation

This is still a very simple example, hopefully these were the last surprises and now I can give it a try with real production code.