Dokumentation und Testen der REST-API mit SpringRestDocs

Guten Tag, ich möchte auf das Thema der Dokumentation der REST-API eingehen. Dieses Material wird sich ab sofort an Ingenieure richten, die im Frühlingsökosystem arbeiten.
Bei den letzten Projekten, bei denen ich das SpringRestDocs-Framework verwendet habe, wurde es erfolgreich im Portfolio fixiert, es wurde Bekannten gezeigt, die ebenfalls damit begonnen haben, es erfolgreich zu verwenden, und jetzt möchte ich mit Ihnen in einem Artikel über seine Fähigkeiten und Vorteile sprechen. In diesem Artikel erfahren Sie, wie Sie SpringRestDocs verwenden und verwenden können.

Von dem Moment an, als ich mit diesem Tool vertraut wurde, stellte ich fest, dass es eine Lösung gab, auf die ich gewartet hatte und die in der Entwicklung nicht ausreichte. Überzeugen Sie sich selbst - davon können Sie nur träumen:

  • Die Dokumentation wird automatisch generiert, wenn Tests ausgeführt werden.
  • Sie können das Quellformat der Dokumentationsdatei steuern, z. B. HTML kompilieren. Da wir Spring Boot verwenden, können wir die Schritte und Aufgaben in gradle ändern, die Dokumentationsdatei kopieren und in die JAR-Datei aufnehmen, die Dokumentationsdokumentation auf dem Remote-Server erstellen und die Dokumentation in das Archiv kopieren. So haben Sie immer einen statischen Endpunkt mit Dokumentation, wo auch immer Ihr Service bereitgestellt wird. Für die Offline-Version können Sie PDF, Epub und Abook-Berechtigungen verbinden.
  • Die Dokumentation unseres REST-Service entspricht garantiert der Arbeitslogik. Die Dokumentation wird mit der Anwendungslogik synchronisiert. Vorgenommene Änderungen, vergessen, sie in der Dokumentation wiederzugeben - Sie sehen sofort fallende Tests mit einer detaillierten Beschreibung des Unterschieds der Nichteinhaltung.
  • Die Dokumentation wird aus Tests generiert. Um der Dokumentation neue Abschnitte hinzuzufügen oder damit zu beginnen, müssen Sie zunächst einen Test schreiben, ja einen Test. In der Tat schreiben Entwickler unter Zeitmangelbedingungen, bei nicht gelieferten Prozessen im Projekt oder aus anderen Gründen sehr oft tonnenweise Code, achten jedoch nicht auf die Wichtigkeit von Tests. Seltsamerweise ermutigt Sie das Dokumentations-Framework, an TDD zu arbeiten
  • Infolgedessen bleibt die Deckung hoch. Genauer gesagt, sind es nicht die Abdeckungsprozentsätze, die in Codeanalysesystemen oder Berichten aufgezeichnet werden, die wichtig sind. Es ist wichtig, dass Sie verschiedene Szenarien mit separaten Tests abdecken und deren Ergebnisse in die Dokumentation aufnehmen. Grüne Tests sind immer ein Vergnügen.
Lassen Sie uns die Arbeit von SpringRestDocs verstehen. Ich werde das Material mit theoretischen Einfügungen kombinieren und den praktischen Teil des Tutorials leiten, nachdem Sie gelesen haben, dass Sie das Framework konfigurieren und verwenden können.

SpringRestDocs-Pipeline


Um mit SpringRestDocs arbeiten zu können, müssen Sie das Funktionsprinzip der Pipeline verstehen. Es ist recht einfach und linear:

Rest Docs Pipeline

Alle Aktionen stammen aus Tests, mit Ausnahme der Logik der Ressourcenüberprüfung, und es werden auch Snippets generiert. Snippets sind der serialisierte Wert eines bestimmten HTTP-Attributs, mit dem unser Controller interagiert hat. Wir bereiten eine spezielle Vorlagendatei vor, in der wir angeben, in welchen Abschnitten die generierten Snippets enthalten sein sollen. Die Ausgabe ist eine kompilierte Dokumentationsdatei. Bitte beachten Sie, dass wir das Dokumentationsformat festlegen können - es kann das Format html, pdf, epub, abook haben.

Im weiteren Verlauf des Artikels werden wir diese Pipeline sammeln, Tests schreiben, SpringRestDocs konfigurieren und die Dokumentation kompilieren.

Abhängigkeiten


Nachfolgend finden Sie die Abhängigkeiten aus meinem Projekt, das mit Federrest-Dokumenten arbeitet, an deren Beispiel wir die Arbeit analysieren werden

dependencies {
    compile "org.springframework.boot:spring-boot-starter-data-jpa"
    compile "org.springframework.boot:spring-boot-starter-hateoas"
    compile "org.springframework.boot:spring-boot-starter-web"
    compile "org.springframework.restdocs:spring-restdocs-core:$restdocsVersion"
    compile "com.h2database:h2:$h2Version"
    compile "org.projectlombok:lombok"
    testCompile "org.springframework.boot:spring-boot-starter-test"
    asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion"
    testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion"
    testCompile "com.jayway.jsonpath:json-path"
}

Testbarer Controller


Ich werde den Teil des Controllers zeigen, für den wir einen Test schreiben, SpringRestDocs verbinden und Dokumentation generieren.

@RestController
@RequestMapping("/speakers")
public class SpeakerController {
    @Autowired
    private SpeakerRepository speakerRepository;
    @GetMapping(path = "/{id}")
    public ResponseEntity getSpeaker(@PathVariable long id) {
        return speakerRepository.findOne(id)
                .map(speaker -> ResponseEntity.ok(new SpeakerResource(speaker)))
                .orElse(new ResponseEntity(HttpStatus.NOT_FOUND));
    }

Werfen wir einen Blick auf die Logik. Mit Hilfe von SpringDataRepository gehe ich zur Datenbank für den Datensatz mit der ID, die an den Controller übergeben wurde. SpringDataRepository gibt Optional zurück - Wenn ein Wert darin enthalten ist, transformieren wir die JPA-Entität in eine Ressource (gleichzeitig können wir einige der Felder einkapseln, die in der Antwort nicht angezeigt werden sollen). Wenn Optional.isEmpty (), geben wir 404 NOT_FOUND-Code zurück .

SpeakerResource-Ressourcencode


@NoArgsConstructor
@AllArgsConstructor
@Getter
@Relation(value = "speaker", collectionRelation = "speakers")
public class SpeakerResource extends ResourceSupport {
    private String name;
    private String company;
    public SpeakerResource(Speaker speaker) {
        this.name = speaker.getName();
        this.company = speaker.getCompany();
        add(linkTo(methodOn(SpeakerController.class).getSpeaker(speaker.getId())).withSelfRel());
        add(linkTo(methodOn(SpeakerController.class).getSpeakerTopics(speaker.getId())).withRel("topics"));
    }
}

Lassen Sie uns einen Basistest für diesen Endpunkt schreiben.


@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs(outputDir = "build/generated-snippets")
public class SpControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private SpeakerRepository speakerRepository;
    @After
    public void tearDown() {
        speakerRepository.deleteAll();
    }
    @Test
    public void testGetSpeaker() throws Exception {
        // Given
        Speaker speaker = Speaker.builder().name("Roman").company("Lohika").build();
        speakerRepository.save(speaker);
        // When
        ResultActions resultActions = mockMvc.perform(get("/speakers/{id}", speaker.getId()))
                .andDo(print());
        // Then
        resultActions.andExpect(status().isOk())
                .andExpect(jsonPath("name", is("Roman")))
                .andExpect(jsonPath("company", is("Lohika")));
    }
}

Im Test verbinde ich die Autokonfiguration mockMVC, RestDocs. Für Restdocs müssen Sie das Verzeichnis angeben, in dem die Snippets generiert werden (outputDir = "buid / generated-snippets") . Dies ist ein normaler Test mit mockMvc, den wir fast täglich schreiben, wenn wir Rest-Services testen. Ich verwende die proprietäre Bibliothek von spring.tests mockMvc Dependence. Wenn Sie jedoch RestAssured bevorzugen, ist auch alles, was Sie lesen, relevant - es gibt nur geringfügige Änderungen. Mein Test ruft die HTTP-Methode des Controllers auf, überprüft den Status, die Felder und druckt den Anforderungs- / Antwortfluss an die Konsole.

ErgebnisseHandler


Nach dem Ausführen des Tests in seiner Ausgabe sehen wir Folgendes:

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /speakers/1
       Parameters = {}
          Headers = {}
Handler:
             Type = smartjava.domain.speaker.SpeakerController
           Method = public org.springframework.http.ResponseEntity smartjava.domain.speaker.SpeakerController.getSpeaker(long)
Async:
    Async started = false
     Async result = null
Resolved Exception:
             Type = null
ModelAndView:
        View name = null
             View = null
            Model = null
FlashMap:
       Attributes = null
MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[application/hal+json;charset=UTF-8]}
     Content type = application/hal+json;charset=UTF-8
             Body = {
  "name" : "Roman",
  "company" : "Lohika",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/speakers/1"
    },
    "topics" : {
      "href" : "http://localhost:8080/speakers/1/topics"
    }
  }
}

Dies ist die Ausgabe des HTTP-Anforderungs- und -Antwortinhalts an die Konsole. Auf diese Weise können wir verfolgen, welche Werte an unsere Steuerung gesendet wurden und wie diese darauf reagiert hat. Die Ausgabe an die Konsole wird vom verbundenen Handler ausgeführt:

 resultActions.andDo(print());

ResultHandler ist eine funktionale Schnittstelle. Nachdem wir unsere eigene Implementierung erstellt und im Test verbunden haben, können wir auf die im Test ausgeführte HttpRequest / HttpResponse zugreifen und die Ergebnisse der Ausführung interpretieren, wenn Sie diese Werte in der Konsole, im Dateisystem, in Ihrer eigenen Dokumentationsdatei usw. aufzeichnen möchten.

public interface ResultHandler {
	/**
	 * Perform an action on the given result.
	 *
	 * @param result the result of the executed request
	 * @throws Exception if a failure occurs
	 */
	void handle(MvcResult result) throws Exception;
}

Mvcresult


Wie wir sehen, hat ResultHandler Zugriff und kann die Werte von MvcResult interpretieren - ein Objekt, das die Ergebnisse des mockMvc-Tests enthält, und über dieses die Attribute von zwei Hauptakteuren - MockHttpServletRequest, MockHttpServletResponse. Hier ist eine

mvcResults

unvollständige Liste dieser Attribute: Hier ist ein Beispiel für MyResultHandler, das den Typ der aufgerufenen HTTP-Methode und den Status des Antwortcodes protokolliert:

public class MyResultHandler implements ResultHandler {
    private Logger logger = LoggerFactory.getLogger(MyResultHandler.class);
    static public ResultHandler myHandler() {
        return new MyResultHandler();
    }
    @Override
    public void handle(MvcResult result) throws Exception {
        MockHttpServletRequest request = result.getRequest();
        MockHttpServletResponse response = result.getResponse();
        logger.error("HTTP method: {}, status code: {}", request.getMethod(), response.getStatus());
    }
}

 resultActions.andDo(new MyResultHandler())

Diese Idee bei der Verarbeitung und Registrierung hat Pivotal zur Erstellung der Dokumentation verwendet. In unserem Test müssen wir einen Handler aus der MockMvcRestDocumentation-Klasse verbinden:

// Document
        resultActions.andDo(MockMvcRestDocumentation.document("{class-name}/{method-name}"));

Lassen Sie uns Schnipsel erzeugen


Führen Sie den Test erneut aus und achten Sie darauf, dass die Ordner mit den Dateien nach ihrer Ausführung im Verzeichnis build / generated-snippets erstellt wurden :

./sp-controller-test/test-get-speaker:
total 48
-rw-r--r--  1 rtsypuk  staff    68B Oct 31 14:17 curl-request.adoc
-rw-r--r--  1 rtsypuk  staff    87B Oct 31 14:17 http-request.adoc
-rw-r--r--  1 rtsypuk  staff   345B Oct 31 14:17 http-response.adoc
-rw-r--r--  1 rtsypuk  staff    69B Oct 31 14:17 httpie-request.adoc
-rw-r--r--  1 rtsypuk  staff    36B Oct 31 14:17 request-body.adoc
-rw-r--r--  1 rtsypuk  staff   254B Oct 31 14:17 response-body.adoc

Dies sind die generierten Snippets. Standardmäßig generiert rest docs 6 Arten von Snippets, von denen ich einige zeigen werde.

Snippet ist ein Teil der HTTP-Anforderungs- / Antwortkomponente, die in einer Datei in einer Textdarstellung serialisiert ist. Die am häufigsten verwendeten Snippets sind curl-request, http-request, http-response, request-body, response-body, Links (für HATEOAS-Dienste), Pfadparameter, Antwortfelder, Header.

curl-request.adoc

[source,bash]
----
$ curl 'http://localhost:8080/speakers/1' -i
----


http-request.adoc
[source,bash]
[source,http,options="nowrap"]
----
GET /speakers/1 HTTP/1.1
Host: localhost:8080
----

http-response.adoc

[source,bash]
[source,http,options="nowrap"]
----
HTTP/1.1 200 OK
Content-Type: application/hal+json;charset=UTF-8
Content-Length: 218
{
  "name" : "Roman",
  "company" : "Lohika",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/speakers/1"
    },
    "topics" : {
      "href" : "http://localhost:8080/speakers/1/topics"
    }
  }
}
----

Vorbereiten der Vorlagendatei


Nun müssen Sie die Vorlagendatei vorbereiten und ihre Abschnitte markieren, in denen die generierten Snippet-Blöcke enthalten sein werden. Die Vorlage wird in einem flexiblen Asciidoc-Format verwaltet. Standardmäßig sollte sich die Vorlage im Verzeichnis src / docs / asciidoc befinden :

== Rest convention
include::etc/rest_conv.adoc[]
== Endpoints
=== Speaker
==== Get speaker by ID
===== Curl example
include::{snippets}/sp-controller-test/test-get-speaker/curl-request.adoc[]
===== HTTP Request
include::{snippets}/sp-controller-test/test-get-speaker/http-request.adoc[]
===== HTTP Response
====== Success HTTP responses
include::{snippets}/sp-controller-test/test-get-speaker/http-response.adoc[]
====== Response fields
include::{snippets}/sp-controller-test/test-get-speaker/response-fields.adoc[]
====== HATEOAS links
include::{snippets}/sp-controller-test/test-get-speaker/links.adoc[]

Mit der asciidoc-Syntax können statische Dateien angehängt werden (z. B. habe ich in der Datei rest_conv.adoc beschrieben, welche Methoden der Dienst unterstützt und in welchen Fällen welche Statuscodes zurückgegeben werden sollen) sowie automatisch generierte Snippets.

Static rest_conv.adoc


=== HTTP verbs
Speakers Service tries to adhere as closely as possible to standard HTTP and REST conventions in its
use of HTTP verbs.
|===
| Verb | Usage
| `GET`
| Used to retrieve a resource
| `POST`
| Used to create a new resource
| `PATCH`
| Used to update an existing resource, including partial updates
| `PUT`
| Used to update an existing resource, full updates only
| `DELETE`
| Used to delete an existing resource
|===
=== HTTP status codes
Speakers Service tries to adhere as closely as possible to standard HTTP and REST conventions in its
use of HTTP status codes.
|===
| Status code | Usage
| `200 OK`
| Standard response for successful HTTP requests.
 The actual response will depend on the request method used.
 In a GET request, the response will contain an entity corresponding to the requested resource.
 In a POST request, the response will contain an entity describing or containing the result of the action.
| `201 Created`
| The request has been fulfilled and resulted in a new resource being created.
| `204 No Content`
| The server successfully processed the request, but is not returning any content.
| `400 Bad Request`
| The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
| `404 Not Found`
| The requested resource could not be found but may be available again in the future. Subsequent requests by the client are permissible.
| `409 Conflict`
| The request could not be completed due to a conflict with the current state of the target resource.
| `422 Unprocessable Entity`
| Validation error has happened due to processing the posted entity.
|===

Konfigurieren Sie build.gradle


Damit die Dokumentation kompiliert werden kann, müssen Sie eine Grundkonfiguration vornehmen - die erforderlichen Abhängigkeiten verbinden. Das ASCIDOctor-Gradle-Plugin muss zu gradle.build buildscript.dependencies hinzugefügt werden

buildscript {
    repositories {
        jcenter()
        mavenCentral()
        mavenLocal()
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        classpath "org.asciidoctor:asciidoctor-gradle-plugin:$asciiDoctorPluginVersion"
        classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
    }
}

Wende das Plugin an

apply plugin: 'org.asciidoctor.convert'

Jetzt müssen wir die grundlegende Asciidoctor-Konfiguration vornehmen:

asciidoctor {
    dependsOn test
    backends = ['html5']
    options doctype: 'book'
    attributes = [
            'source-highlighter': 'highlightjs',
            'imagesdir'         : './images',
            'toc'               : 'left',
            'toclevels'         : 3,
            'numbered'          : '',
            'icons'             : 'font',
            'setanchors'        : '',
            'idprefix'          : '',
            'idseparator'       : '-',
            'docinfo1'          : '',
            'safe-mode-unsafe'  : '',
            'allow-uri-read'    : '',
            'snippets'          : snippetsDir,
            linkattrs           : true,
            encoding            : 'utf-8'
    ]
    inputs.dir snippetsDir
    outputDir "build/asciidoc"
    sourceDir 'src/docs/asciidoc'
    sources {
        include 'index.adoc'
    }
}

Lassen Sie uns die Dokumentationsassembly überprüfen und in der Konsole ausführen

gradle asciidoctor

Da wir angegeben haben, dass die Asciidoctor-Aufgabe von der Ausführung der Tests abhängt, werden zuerst die Tests durchbrochen, Snippets generiert und diese Snippets werden in die generierte Dokumentation aufgenommen.

Die Dokumentation


Alle beschriebenen Konfigurationsschritte müssen einmalig beim Anheben des Projekts durchgeführt werden. Jedes Mal, wenn wir Tests durchführen, generieren wir zusätzlich Snippets und Dokumentationen. Hier einige Screenshots:

Abschnitt "Vereinbarung" zu HTTP-Methoden und Statuscodes

Bild

Beispieldokumentation für die Methode "Get All Speakers" Eine

Bild

identische Dokumentation ist auch im PDF-Format verfügbar. Es ist praktisch als Offline-Version und kann zusammen mit den Spezifikationen Ihres Dienstes an die Kunden gesendet werden.

Bild

Änderung der Glasaufgabe


Nun, da wir mit Spring Boot arbeiten, können wir jetzt eine seiner interessanten Eigenschaften verwenden - alle Ressourcen, die sich im Verzeichnis src / static oder src / public befinden, sind als statischer Inhalt verfügbar, wenn über den Browser darauf zugegriffen wird

jar {
    dependsOn asciidoctor
    from ("${asciidoctor.outputDir}/html5") {
        include '**/index.html'
        include '**/images/*'
        into 'static/docs'
    }
}

Genau das werden wir tun - nachdem wir die Dokumentation zusammengestellt haben, werden wir sie in das Verzeichnis / static / docs kopieren. Somit enthält jedes gesammelte Glasartefakt einen statischen Endpunkt mit Dokumentation. Unabhängig davon, wo es bereitgestellt wird, in welcher Umgebung es sich befindet, ist die aktuelle Version der Dokumentation immer verfügbar.

Fazit


Dies ist nur ein kleiner Teil der Möglichkeiten dieses wunderbaren Tools. Es ist unmöglich, alles in einem Artikel zu behandeln. Für alle, die sich für SpringRestDocs interessieren, biete ich Links zu Ressourcen an:

  • So sieht die kompilierte Dokumentation aus, in diesem Beispiel können Sie sich das ASCII-Format ansehen, wie leistungsfähig dieses Tool ist (Sie können das Dock übrigens automatisch auf githubpages herunterladen). tsypuk.github.io/springrestdoc
  • my github mit einem benutzerdefinierten Demo-Projekt mit SpringRestDocs github.com/tsypuk/springrestdoc (alles ist konfiguriert, verwenden Sie den Code in Ihren Projekten für einen schnellen Start, es gibt Demo-Syntax asciidoctor, Beispiele für Erweiterungen, Diagramme, die einfach generiert und in die Dokumentation aufgenommen werden können)
  • Und natürlich die offizielle Dokumentation

Jetzt auch beliebt: