Asciidoc with Spring Restdocs

integrate asciidoc with spring restdocs

Integrate Asciidoc with Spring Restdocs

Preconditon

The following is based on Webflux

1
2
3
asciidoctor version: 3.2.0
spring restdocs version: 2.0.4.RELEASE
dependency management: gradle

Some asciidoctor knowledge:

sourceDir: src/docs/asciidoc

outputDir: ${buildDir}/docs/asciidoc

Configuration

common config

The following config is not a complete configuration.

 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
// plugins
plugins {
    id 'org.springframework.boot' version '2.3.0.M4'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
    /* for rest docs */
    id 'org.asciidoctor.jvm.convert' version '3.2.0'
}

configurations {
    asciidoctorExtensions
}

ext {
    set('snippetsDir', file('build/generated-snippets'))
    set('springRestdocsVersion', '2.0.4.RELEASE')
}

dependencies {
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'io.projectreactor:reactor-test'
    /* for rest docs */
    testImplementation "org.springframework.restdocs:spring-restdocs-webtestclient:${springRestdocsVersion}"
    asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:${springRestdocsVersion}"
}

test {
    outputs.dir snippetsDir
    useJUnitPlatform()
}

asciidoctor {
    configurations 'asciidoctorExtensions'
    dependsOn test
    attributes 'snippets': snippetsDir
    inputs.dir snippetsDir
}

bootJar {
    dependsOn asciidoctor
    from("${asciidoctor.outputDir}") {
        into 'static/docs'
    }
}

create docs/asciidoc/ directory

like this

directory_layout

index.adoc & updateUser.adoc(e.g.)

index.adoc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
= Blog Restful API
Purple Mystic;
:toc: left
:toc-title: Chapter
:doctype: book
:icons: font
:source-highlighter: highlightjs
:sourcedir: {sourcedir}/user

include::createUser.adoc[]
include::updateUser.adoc[]
include::findUserById.adoc[]
include::findAllUsers.adoc[]

updateUser.adoc

 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
== *Backend: updateUser:*

=== Curl request:

include::{snippets}/updateUser/curl-request.adoc[]

=== HTTP request:

include::{snippets}/updateUser/http-request.adoc[]

=== HTTP response:

include::{snippets}/updateUser/http-response.adoc[]

=== Request using HTTPie:

include::{snippets}/updateUser/httpie-request.adoc[]

=== Request body:

include::{snippets}/updateUser/request-body.adoc[]

=== Response body:

include::{snippets}/updateUser/response-body.adoc[]

Other solution

  • remove :sourcedir: {sourcedir}/user from index.adoc
  • reconfigure asciidoctor task in build.gradle, like this
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
asciidoctor {
    configurations 'asciidoctorExtensions'
    dependsOn test
    attributes 'snippets': snippetsDir
    inputs.dir snippetsDir
    sources {
        include '**/index.adoc'
    }
    baseDirFollowsSourceFile()
}

Unit Tests

  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
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
package team.star.blog.controller;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.ApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import team.star.blog.pojo.User;
import team.star.blog.service.UserService;

import java.util.List;

import static org.mockito.Mockito.when;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration;

/**
 * @author mystic
 */
@WebFluxTest(UserController.class)
@ExtendWith(RestDocumentationExtension.class)
public class UserControllerTest {
    @Autowired
    private ApplicationContext context;
    private WebTestClient client;
    @MockBean
    private UserService userService;

    @BeforeEach
    void setUp(RestDocumentationContextProvider provider) {
        client = WebTestClient.bindToApplicationContext(context)
            .configureClient()
            .filter(
                documentationConfiguration(provider)
                    .operationPreprocessors()
                    .withRequestDefaults(prettyPrint())
                    .withResponseDefaults(prettyPrint())
            )
            .build();

        User u1 = User.builder().id(1).name("Mystic").build();
        User u2 = User.builder().id(2).name("Ran").build();

        when(userService.findAll()).thenReturn(Flux.fromIterable(List.of(u1, u2)));
        when(userService.findById(Mockito.anyInt())).thenReturn(Mono.just(u1));
        when(userService.save(Mockito.any(User.class))).thenReturn(Mono.just(u2));
    }

    @Test
    void findUserById() {
        client.get().uri("/user/{id}", 1)
            .exchange()
            .expectStatus().isOk()
            .expectBody(User.class)
            .consumeWith(document("findUserById",
                pathParameters(parameterWithName("id").description("User ID"))
            ));

    }

    @Test
    void findAllUsers() {
        client.get().uri("/user").exchange()
            .expectStatus().isOk()
            .expectBodyList(User.class)
            .consumeWith(document("findAllUsers"));
    }

    @Test
    void createUser() {
        User u3 = User.builder().name("cc").build();
        client.post().uri("/user")
            .contentType(MediaType.APPLICATION_JSON)
            .accept(MediaType.APPLICATION_JSON)
            .body(Mono.just(u3), User.class)
            .exchange()
            .expectStatus().isCreated()
            .expectBody()
            .jsonPath("$.id").isEqualTo(2)
            .consumeWith(document("createUser"));
    }

    @Test
    void updateUser() {
        User u2 = User.builder().id(2).name("cc").build();
        client.patch().uri("/user")
            .contentType(MediaType.APPLICATION_JSON)
            .accept(MediaType.APPLICATION_JSON)
            .body(Mono.just(u2), User.class)
            .exchange()
            .expectStatus().isOk()
            .expectBody()
            .jsonPath("$.name").isEqualTo("Ran")
            .consumeWith(document("updateUser"));
    }
}

Source Code

https://github.com/PurpleMystic-star/blog-backend

Licensed under CC BY-NC-SA 4.0
Last updated on Aug 18, 2023 13:13 UTC