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