ABONAMENTE VIDEO REDACȚIA
RO
EN
NOU
Numărul 150
Numărul 149 Numărul 148 Numărul 147 Numărul 146 Numărul 145 Numărul 144 Numărul 143 Numărul 142 Numărul 141 Numărul 140 Numărul 139 Numărul 138 Numărul 137 Numărul 136 Numărul 135 Numărul 134 Numărul 133 Numărul 132 Numărul 131 Numărul 130 Numărul 129 Numărul 128 Numărul 127 Numărul 126 Numărul 125 Numărul 124 Numărul 123 Numărul 122 Numărul 121 Numărul 120 Numărul 119 Numărul 118 Numărul 117 Numărul 116 Numărul 115 Numărul 114 Numărul 113 Numărul 112 Numărul 111 Numărul 110 Numărul 109 Numărul 108 Numărul 107 Numărul 106 Numărul 105 Numărul 104 Numărul 103 Numărul 102 Numărul 101 Numărul 100 Numărul 99 Numărul 98 Numărul 97 Numărul 96 Numărul 95 Numărul 94 Numărul 93 Numărul 92 Numărul 91 Numărul 90 Numărul 89 Numărul 88 Numărul 87 Numărul 86 Numărul 85 Numărul 84 Numărul 83 Numărul 82 Numărul 81 Numărul 80 Numărul 79 Numărul 78 Numărul 77 Numărul 76 Numărul 75 Numărul 74 Numărul 73 Numărul 72 Numărul 71 Numărul 70 Numărul 69 Numărul 68 Numărul 67 Numărul 66 Numărul 65 Numărul 64 Numărul 63 Numărul 62 Numărul 61 Numărul 60 Numărul 59 Numărul 58 Numărul 57 Numărul 56 Numărul 55 Numărul 54 Numărul 53 Numărul 52 Numărul 51 Numărul 50 Numărul 49 Numărul 48 Numărul 47 Numărul 46 Numărul 45 Numărul 44 Numărul 43 Numărul 42 Numărul 41 Numărul 40 Numărul 39 Numărul 38 Numărul 37 Numărul 36 Numărul 35 Numărul 34 Numărul 33 Numărul 32 Numărul 31 Numărul 30 Numărul 29 Numărul 28 Numărul 27 Numărul 26 Numărul 25 Numărul 24 Numărul 23 Numărul 22 Numărul 21 Numărul 20 Numărul 19 Numărul 18 Numărul 17 Numărul 16 Numărul 15 Numărul 14 Numărul 13 Numărul 12 Numărul 11 Numărul 10 Numărul 9 Numărul 8 Numărul 7 Numărul 6 Numărul 5 Numărul 4 Numărul 3 Numărul 2 Numărul 1
×
▼ LISTĂ EDIȚII ▼
Numărul 49
Abonament PDF

Un exemplu cu Spring REST Docs

Vasile Boris
Programator, team lead, trainer & coach



PROGRAMARE

În luna mai din acest an, am participat la conferința Spring I/O din Barcelona unde am auzit de Spring REST docs. Două idei mi-au atras atenția:

După conferință am căutat mai multe informații în documentația Spring REST docs și am creat un exemplu de serviciu REST ca să pun în practică. Exemplul este foarte simplu și este clasicul Hello World.

public class Greeting {

    private final long id;
    private final String content;

    public Greeting(long id, String content) {
        this.id = id;
        this.content = content;
    }

    public long getId() {
        return id;
    }

    public String getContent() {
        return content;
    }

}

@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));
    }

}

Spring REST docs folosește Asciidoctor să genereze documentația prin urmare configurarea maven a fost primul pas.

<plugin>
    <groupId>org.asciidoctor</groupId>
    <artifactId>asciidoctor-maven-plugin</artifactId>
    <version>1.5.2</version>
    <executions>
        <execution>
            <id>generate-docs</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>process-asciidoc</goal>
            </goals>
            <configuration>
                <backend>html</backend>
                <doctype>book</doctype>
                <attributes>
                    <snippets>${project.build.directory}/generated-snippets</snippets>
                </attributes>
                <sourceDirectory>src/docs/asciidocs</sourceDirectory>
                <outputDirectory>target/generated-docs</outputDirectory>
            </configuration>
        </execution>
    </executions>
</plugin>

Informația relevantă din această configurare este:

Următorul pas este să creez clasa de test:

@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")
                )));

    }

}

Documentația este generată de această clasă de test în doi pași:

Prima metodă de test greetingGetWithProvidedContent() verifică ce se întâmplă dacă acest serviciu este apelat cu parametrul name setat:

A doua metodă de test greetingGetWithDefaultContent() verifică ce se întâmplă dacă serviciul este apelat fără parametrul name. În acest caz are sens să documentez doar ce este diferit:

Fragmentele de documentație sunt generate de clasele de test și avem nevoie de o cale de a le îmbina. Acum este momentul când avem nevoie de cunoștințele noastre de Asciidoctor. Trebuie sa creăm un fișier în directorul src/docs/asciidocs (vezi sourceDirectory din configurarea asciidoctor-maven-plugin):

= 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[]

Cel mai important aspect este includerea fragmentelor generate. Fiecare metodă de test generează implicit trei fragmente:

Pe lângă aceste trei fragmente implicite metodele noastre de test generează două fragmente adiționale:

După ce se rulează mvn clean package documentația generată poate fi găsită în directorul target/generated-docs:

http://espressoprogrammer.com/blog/wp-content/uploads/2016/06/greeting-service-api-guide.html

Am generat documentația, hai să verificăm ce se întâmplă când instrucțiunile de documentare nu sunt actualizate odată cu implementarea.

Primul scenariu este să schimb parametrul din cerere din name în content. Am actualizat controlerul și modul în care metoda de test accesează resursa dar am uitat să actualizez instrucțiunile de documentare.

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

@Test
public void greetingGetWithProvidedContent() throws Exception {

    this.mockMvc.perform(get("/greeting"). param("content", "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()
            )));

}

Testul eșuează cu:

org.springframework.restdocs.snippet.SnippetException:
Request parameters with the following names were not documented: [content].
Request parameters with the following names were not found in the request: [name]

Al doilea scenariu este să omit să documentez un câmp. În greetingGetWithDefaultContent() nu voi documenta câmpul id:

@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("content").description("When name is not provided, this field contains the default value")
            )));

}

Testul eșuează cu:

org.springframework.restdocs.snippet.SnippetException: The following parts of the payload were not documented:
{
  "id" : 1
}

Ultimul scenariu este să documentez un câmp care nu este returnat. Voi șterge optional() din documentarea câmpului optionalContent din metoda greetingGetWithProvidedContent():

@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)
            )));

}

Testul eșuează cu:

org.springframework.restdocs.snippet.SnippetException:
Fields with the following paths were not found in the payload: [optionalContent]

Nu am folosit încă Spring REST docs să documentez un serviciu real din producție, dar am de gând să fac o încercare.

Referințe

  1. Spring REST Docs - http://projects.spring.io/spring-restdocs/
  2. Asciidoctor - http://asciidoctor.org/

NUMĂRUL 149 - Development with AI

Sponsori

  • Accenture
  • BT Code Crafters
  • Accesa
  • Bosch
  • Betfair
  • MHP
  • BoatyardX
  • .msg systems
  • P3 group
  • Ing Hubs
  • Cognizant Softvision
  • Colors in projects