After my previous post I thought that there were no more surprises and I would be able to document an already existing endpoint that I had written in the past. How wrong I was. I updated my example to be similar with that endpoint so that I could explain what happened.
The first difference is that the endpoint returns a list of greetings:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@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 List<Greeting> greeting(@QueryParam("name") @DefaultValue("World") String name) { return Arrays.asList(new Greeting(counter.incrementAndGet(), String.format(template, name))); } } |
The second difference is that spring is used instead of spring-boot and the application is deployed as a war file in tomcat. The important project files are listed below:
web.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <display-name>greeting-service</display-name> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <listener> <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class> </listener> <servlet> <servlet-name>Jersey Spring Servlet</servlet-name> <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class> <init-param> <param-name>com.sun.jersey.config.property.resourceConfigClass</param-name> <param-value>com.sun.jersey.api.core.PackagesResourceConfig</param-value> </init-param> <init-param> <param-name>com.sun.jersey.config.property.packages</param-name> <param-value>com.espressoprogrammer.hello;org.codehaus.jackson.jaxrs</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>Jersey Spring Servlet</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app> |
applicationContext.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.espressoprogrammer.hello"/> </beans> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
@XmlRootElement public class Greeting { private long id; private String content; public Greeting() { } public Greeting(long id, String content) { this.id = id; this.content = content; } public void setId(long id) { this.id = id; } public long getId() { return id; } public void setContent(String content) { this.content = content; } public String getContent() { return content; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@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 List<Greeting> greeting(@QueryParam("name") @DefaultValue("World") String name) { return Arrays.asList(new Greeting(counter.incrementAndGet(), String.format(template, name))); } } |
The third difference is that endpoint is tested with jersey api client:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"classpath:/test-applicationContext.xml"}) public class GreetingServiceJerseyClientOldTest extends JerseyTest { private static final String JERSEY_TEST_PORT_SYSTEM_PROPERTY = "jersey.test.port"; private static final Integer JERSEY_TEST_PORT = 8080; @BeforeClass public static void init() { System.setProperty(JERSEY_TEST_PORT_SYSTEM_PROPERTY, JERSEY_TEST_PORT.toString()); } @Rule public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); public GreetingServiceJerseyClientOldTest() { super(new WebAppDescriptor.Builder("com.espressoprogrammer.hello") .contextPath("greeting-service") .contextParam("contextConfigLocation", "classpath:/test-applicationContext.xml") .servletClass(SpringServlet.class) .contextListenerClass(ContextLoaderListener.class) .build()); } @Test public void greetingGetWithProvidedContent() throws Exception { WebResource webResource = resource().path("greeting"); ClientResponse response = webResource .queryParam("name", "Everybody") .accept(MediaType.APPLICATION_JSON) .get(ClientResponse.class); assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); List<Greeting> greetings = response.getEntity(new GenericType<List<Greeting>>(){}); assertThat(greetings).hasSize(1); assertThat(greetings.get(0).getContent()).isEqualTo("Hello, Everybody!"); } @Test public void greetingGetWithDefaultContent() throws Exception { WebResource webResource = resource().path("greeting"); ClientResponse response = webResource .accept(MediaType.APPLICATION_JSON) .get(ClientResponse.class); assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); List<Greeting> greetings = response.getEntity(new GenericType<List<Greeting>>(){}); assertThat(greetings).hasSize(1); assertThat(greetings.get(0).getContent()).isEqualTo("Hello, World!"); } } |
The first attempt to update the test class to use spring-restdocs and rest-assured went well until I got to the point of documenting response fields and I noticed that the service is returning:
1 2 |
{"greeting":{"content":"Hello, Everybody!","id":"1"}} |
instead of the expected:
1 2 |
[{"id":1,"content":"Hello, Everybody!"}] |
After spending some time and using a lot of #$@&%*! I found that in web.xml jersey is configured to use jackson:
1 2 3 4 5 |
<init-param> <param-name>com.sun.jersey.config.property.packages</param-name> <param-value>com.espressoprogrammer.hello;org.codehaus.jackson.jaxrs</param-value> </init-param> |
The corresponding code from jersey test class is:
1 2 3 4 5 6 7 8 9 |
public GreetingServiceBadTest() { super(new WebAppDescriptor.Builder("com.espressoprogrammer.hello") .contextPath("greeting-service") .contextParam("contextConfigLocation", "classpath:/test-applicationContext.xml") .servletClass(SpringServlet.class) .contextListenerClass(ContextLoaderListener.class) .build()); } |
where I missed to add jackson and jaxb was used instead. The fix was simple:
1 2 3 4 5 6 7 8 9 |
public GreetingServiceTest() { super(new WebAppDescriptor.Builder("com.espressoprogrammer.hello;org.codehaus.jackson.jaxrs") .contextPath("greeting-service") .contextParam("contextConfigLocation", "classpath:/test-applicationContext.xml") .servletClass(SpringServlet.class) .contextListenerClass(ContextLoaderListener.class) .build()); } |
and the response was the expected one:
1 2 |
[{"id":1,"content":"Hello, Everybody!"}] |
The final test class looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"classpath:/test-applicationContext.xml"}) public class GreetingServiceTest extends JerseyTest { private static final String JERSEY_TEST_PORT_SYSTEM_PROPERTY = "jersey.test.port"; private static final Integer JERSEY_TEST_PORT = 8080; @BeforeClass public static void init() { System.setProperty(JERSEY_TEST_PORT_SYSTEM_PROPERTY, JERSEY_TEST_PORT.toString()); } @Rule public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); public GreetingServiceTest() { super(new WebAppDescriptor.Builder("com.espressoprogrammer.hello;org.codehaus.jackson.jaxrs") .contextPath("greeting-service") .contextParam("contextConfigLocation", "classpath:/test-applicationContext.xml") .servletClass(SpringServlet.class) .contextListenerClass(ContextLoaderListener.class) .build()); } @Test public void greetingGetWithProvidedContent() throws Exception { FieldDescriptor[] greeting = new FieldDescriptor[] { fieldWithPath("id").description("Greeting's generated id"), fieldWithPath("content").description("Greeting's content"), fieldWithPath("optionalContent").description("Greeting's optional content").type(JsonFieldType.STRING).optional() }; given() .port(JERSEY_TEST_PORT) .filter(documentationConfiguration(this.restDocumentation)) .filter(document("{class-name}/{method-name}", requestParameters(parameterWithName("name").description("Greeting's target")), responseFields(fieldWithPath("[]").description("An array of greetings")).andWithPrefix("[].", greeting))) .accept(MediaType.APPLICATION_JSON) .when() .get("/greeting-service/greeting?name={id}", "Everybody") .then() .statusCode(HttpStatus.OK.value()) .assertThat().contentType(equalTo(MediaType.APPLICATION_JSON)) .assertThat().body("content", hasItems("Hello, Everybody!")) ; } @Test public void greetingGetWithDefaultContent() throws Exception { FieldDescriptor[] greeting = new FieldDescriptor[] { fieldWithPath("id").ignored(), fieldWithPath("content").description("When name is not provided, this field contains the default value") }; given() .port(JERSEY_TEST_PORT) .filter(documentationConfiguration(this.restDocumentation)) .filter(document("{class-name}/{method-name}", responseFields(fieldWithPath("[]").description("An array of greetings")).andWithPrefix("[].", greeting))) .accept(MediaType.APPLICATION_JSON) .when() .get("/greeting-service/greeting") .then() .statusCode(HttpStatus.OK.value()) .assertThat().contentType(equalTo(MediaType.APPLICATION_JSON)) .assertThat().body("content", hasItems("Hello, World!")) ; } } |
If jersey api client is used to test the endpoint, also the client needs to be configured to use jackson:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
private static ClientConfig createClientConfig() { ClientConfig clientConfig = new DefaultClientConfig(); clientConfig.getClasses().add(JacksonJaxbJsonProvider.class); return clientConfig; } public GreetingServiceJerseyClientTest() { super(new WebAppDescriptor.Builder("com.espressoprogrammer.hello;org.codehaus.jackson.jaxrs") .contextPath("greeting-service") .contextParam("contextConfigLocation", "classpath:/test-applicationContext.xml") .servletClass(SpringServlet.class) .contextListenerClass(ContextLoaderListener.class) .clientConfig(createClientConfig()) .build()); } |
After making these small changes, I was finally able to document a production endpoint.