本次 more time 为读者👬们介绍 Spring Rest Docs,这是一个用代码写接口文档的工具。
先看看效果
和 swagger 不同,swagger 倾向于自动生成,这个还是得写。至于怎么写呢?Spring Rest Docs 的 Github 给出了更加明确的定义:测试驱动的文档。
说到测试驱动,不得不提测试驱动开发 TDD。总之TDD的核心就是:先写测试,后写代码,这样即能保证代码是对的,又敢重构。
那么这个Spring Rest Docs 这个测试驱动的文档是什么意思呢?
写测试,生成文档。
生成文档的前提是测试通过。
流程就是:写测试 -> 写能通过测试的代码 -> 通过测试 -> 生成文档。
问题在 能通过测试的代码 上,这里有两种选择。
- 符合业务需求的正确代码
- 能通过测试的类似Mock代码
如果选择1,那么这份接口文档的交付时间就是正式代码写好之后
如果选择2,那么这个Mock代码就很浪费,因为mockserver 有 wiremock 解决方案,简单的CRUD还有 json-server
之后还有会有 Spring Cloud Contract , 它实现了消费者驱动契约。
如何将这些技术统一起来,形成一个最佳的工作流,是我最近思考的问题。
光就 Spring Rest Docs 来说
方案1:这份文档是用 CI生成的,这份文档会随着测试驱动开发不断丰富,每新写一个 controller 就丰富这个文档,供用户看,是不断变化的。
方案2:感觉是一种反模式,我第一次就是这么用的。先写 controller,无视输入,直接返回一个值,再写好全部测试,有输入输出,最后来生成。这个好处就是可以立刻交付文档,并且因为通过了测试,这一份代码也可以拿去给其他人当mockserver。
本文的小demo并不是涉及复杂逻辑,主要是带领读者👭们看一看这么用。
小王接到了一个任务,写两个接口,一个GET一个POST
GET 算平方
长这样 GET /sqr?x=2
返回 {"ans": 4}
POST 算和
长这样 POST /sum/3 {``"y": 5}
返回 {"ans": 8}
小王说着太简单了,瞬间写好了——测试,用了mockmvc,这样就先不需要实例化 controller 了。
@Test
fun shouldReturn4whenXis2() {
mockMvc.perform(get("/sqr?x=2"))
.andExpect(jsonPath("$.ans", `is`(4)))
}
@Test
fun shouldReturn8whenXis3andYis5() {
val req = """
{
"y": 5
}
""".trimIndent()
mockMvc.perform(post("/sum/3")
.content(req)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.ans", `is`(8)))
}
写好之后小王点了一下运行,啥也没写,果然变红,于是开始写 controller,过了一会儿,写好了。
@GetMapping("/sqr")
fun sqr(@RequestParam x: Int) = Res(x * x)
@PostMapping("/sum/{x}")
fun sum(@PathVariable x: String, @RequestBody req: Req) = Res(x.toInt() + req.y)
再点一下测试,变绿了,都通过了测试,完美。
这时小王想用上了 Spring Rest Docs 来声明文档,首先引入 Maven依赖。如果用 Spring Boot Starter 直接勾就好了,如果是老项目,请点击阅读原文看语雀上的附录。
之后给测试加点料
类前面加上 @AutoConfigureRestDocs
,这时候里面的 mockmvc 已经不是原来那个 mockmvc了,它变得更强了。
如
@AutoConfigureRestDocs
class DemoApplicationTests {
之后给测试后面加上 andDo(Document("docname"))
如
.andExpect(jsonPath("$.ans", `is`(4)))
.andDo(document("sqr"))
之后再次运行测试,神奇的事情发生了,在 target 下生成了 generated-snippets 文件夹,里面还有相关的子文件夹,如 sqr, sum,里面生成相关的 adoc 文件。
adoc文件使用了 asciidoc 的语法,比 markdown 功能更多。具体可以参考 Asciidoc官方网站
但是生成这些片段也很麻烦,莫非 cat *.adoc
把他们整理起来?
当然不用。
在 src/main 下建一个 asciidoc 文件夹,然后新建一个 index.adoc 文件,大概长这样
= 接口文档
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:
== sqr
operation::sqr[]
== sum
operation::sum[]
前面是一些设置,和LaTeX类似。然后 ==
是二级标题, operation
是把 snippets 组合起来。
最后运行mvn package
就会发现在 target下多了一个 generated-docs 文件夹,里面有一个index.html,这就是生成的文档。
不过问题又来了,这就只有一个样例输入样例输出也太弱了,字段类型都没有。
当然有,只不过要再加点编码。
字段信息有 requestParameter, pathParameters, requestFields 和 responseFields 这几类,用法都是先限定名字,然后用 description() 描述内容,代码如下
.andDo(document("sqr",
requestParameters(
parameterWithName("x").description("输入的数")
),
responseFields(
fieldWithPath("ans").description("答案")
)))
.andDo(document("sum",
pathParameters(
parameterWithName("x").description("第一个加数")
),
requestFields(
fieldWithPath("y").description("第二个加数")
),
responseFields(
fieldWithPath("ans").description("它们的和")
)))
这样便完成了字段的编写
如果不想引入全部 snippets,可以用operation::index[snippets='curl-request,http-request,http-response']
这种语法来指定,
补充
- 以后阅读原文会链接到语雀上,毕竟公众号连外链都不行,很闭塞
代码附录
- 需要加的 pom
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.8</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<doctype>book</doctype>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-asciidoctor</artifactId>
<version>${spring-restdocs.version}</version>
</dependency>
</dependencies>
</plugin>
欢迎关注