使用Spring REST Docs记录表单参数
[LOADING...]
1. 引言
表单编码的端点在登录和订阅流程中很常见,但如果没有精确的请求文档,很容易产生误解。API 消费者需要知道哪些参数是必需的,哪些是可选的,哪些值是有效的,以及哪些额外参数是被有意忽略的。
在本教程中,我们将使用 Spring REST Docs 和 MockMvc 测试来为 Spring MVC 端点记录表单参数。
需要注意的是,尽管查询参数与此相关,但它们服务于不同的用例,并且使用不同的 REST Docs 片段进行记录,因此我们在这里不会涉及它们。
2. 为什么要记录表单参数?
在表单提交中,一切都是以键值对的形式开始的。如果没有文档,就会存在歧义:
- 所有字段都是必需的吗?
- 受约束的字段接受哪些值?
- 是否有可忽略的参数?
Spring MVC 为我们处理了所有这些,但 API 消费者仍然需要确切的细节。Spring REST Docs 在被记录的测试通过后生成文档片段。
需要注意的是,REST Docs 不会自动将任意断言转换为文档。我们仍然以编程方式描述每个参数。我们得到的好处是,我们只从通过并验证代码行为的测试中获取文档。
请注意,本文仅处理表单参数,不处理查询参数:
- 表单参数属于编码为 application/x-www-form-urlencoded 的请求体
- 查询参数是 URL 的一部分,例如 /newsletter/subscriptions?frequency=weekly。 它们服务于不同的用例,并使用不同的 REST Docs 片段进行记录。
3. 构建基于表单的端点
让我们构建一个小型新闻订阅 API,它接收表单字段、验证有效负载并返回 JSON 响应。
基本上,我们的表单模型表示来自 application/x-www-form-urlencoded 数据的请求字段。Spring 通过将参数名称与 JavaBean 属性名称进行匹配,将参数绑定到此对象。
那么,让我们从订阅表单开始。我们使用 Bean Validation 注解来强制实施必需和受约束的值:
frequency 的模式明确指出了有效值。如果客户端发送的值不是 weekly 或 monthly,验证将在业务逻辑运行之前失败。请注意,Spring REST Docs 不会推断该约束,因此它不会自动将有效值添加到生成的文档中。
4. 测试设置和请求构建
REST Docs 的主要思想是从经过测试的行为中生成片段。因此,我们使用 MockMvc 和 REST Docs 配置来创建一个包含所有受支持表单字段的请求。
4.1. 测试设置和片段命名
我们的测试类使用 @WebMvcTest,我们还需要指定 RestDocumentationExtension,以便 JUnit 可以管理来自 Spring REST Docs 的 RestDocumentationContext:
我们的 setUp() 方法为每个测试准备上下文:
- apply(documentationConfiguration(restDocumentation)) 将 Spring REST Docs 附加到由 webAppContextSetup() 构建的 MockMvc 实例,以便捕获请求和响应。
- alwaysDo(document("{method-name}")) 添加一个默认的文档生成操作,该操作会在我们测试中的每个 mockMvc.perform() 上运行。 {method-name} 占位符捕获当前测试方法名称。
当我们想要固定的片段目录名称时,我们在测试本身中设置一个显式标识符, 例如 document("newsletter-subscribe")。这将在 target/generated-snippets/newsletter-subscribe 下创建片段。
4.2. 表单请求
让我们分解测试的每个部分,从一个包含表单字段的简单模拟负载开始。我们使用 param() 方法将键值对发送到接受表单负载的端点:
注意,我们使用 param("topics", "spring", "testing") 发送了两次 topics,这就是它处理多值表单字段的方式。
5. 记录表单参数
现在,我们准备好进行文档记录了。
5.1. 描述表单参数
我们使用 formParameters() 来记录表单负载。我们定义名称和描述,对于非必需参数使用 optional():
如果有效的 frequency 值发生变化,我们需要记住手动更新参数描述,因为 @Pattern 注解不会推断它。
最重要的是,optional() 控制 Spring REST Docs 如何记录参数。它不会影响 Bean Validation。当 Spring 验证表单对象时,诸如 @NotBlank 之类的约束会独立应用。
如果我们添加或删除必需参数,通常会发生以下两种情况之一:要么请求验证行为改变并且测试失败,要么描述符匹配失败,因为我们记录的参数不再匹配请求。
5.2. 处理 Bean Validation 约束
为了避免手动描述约束,我们可以通过 ConstraintDescriptions 读取验证元数据,并通过调用 descriptionsForProperty() 将其合并到参数描述中。
让我们创建一个辅助方法来获取 SubscriptionForm 类和 frequency 属性的约束描述:
现在,我们可以回到 docFormParams() 并使用我们的 constraintsFor() 方法:
这是我们得到的描述:
因此,它包含了所有验证注解的描述。如果它们发生变化,描述也会随之变化。
5.3. 定义被忽略的参数
一些客户端发送额外的表单值,我们不想将其包含在 API 文档中。例如,前端跟踪参数与业务逻辑无关。默认情况下,如果 REST Docs 发现任何未记录的参数,它会失败并抛出 SnippetException,因此我们必须显式地忽略它们。
让我们首先回到 postSubscription() 并发送我们稍后将忽略的 trackingId 参数:
现在,在 docFormParams() 中,我们将 trackingId 添加为被忽略的参数:
这样,该参数可以出现,但它不是我们最终获得的 API 文档的一部分。
6. 执行测试并生成片段
最后,让我们将所有这些部分放在一个模拟请求中:
这个单一的测试验证了端点,并为所有表单参数定义了字段级文档。最重要的是,它有助于将契约保持在一个地方。
当我们运行已记录的测试时,Spring REST Docs 会在 target/generated-snippets/newsletter-subscribe 下写入片段文件,这是我们使用 document() 方法选择的名称。
6.1. 生成的表单片段
为我们的表单 POST 测试执行生成的片段以结构化格式显示参数,使用我们通过 parameterWithName() 调用提供的描述:
我们可以在 Maven 构建期间使用 Asciidoctor 插件将此片段包含在 AsciiDoc 页面中。
主要优势在于,我们的片段来自实际在测试中运行的请求。如果测试的行为或描述符不再匹配,测试或片段生成就会失败,迫使我们一起更新代码和文档。
7. 严格模式与宽松模式的参数文档
默认情况下,formParameters(...) 是严格的。这意味着未记录的参数会导致片段失败,除非我们将它们标记为被忽略。
然而,有时我们想要更宽松的约束。例如,我们可能只关心记录核心字段,同时允许请求中包含额外的键。 在这种情况下,我们通过 relaxedFormParameters() 而不是 formParameters() 进入宽松模式。 让我们用 relaxedFormParameters() 编写一个新的辅助方法:
以下是配套的测试方法:
它提交表单,检查返回状态是否为 created,然后在 newsletter-subscribe-relaxed 文件夹中生成文档片段。
7.1. 参数模式快速参考
让我们回顾所有参数处理模式:
在实践中,我们将对稳定的契约使用严格模式,为已知的非契约输入添加 ignored(),并且仅在我们需要额外灵活性时切换到宽松模式。
8. 结论
在本文中,我们使用 Spring REST Docs 和测试优先的工作流程记录了一个表单编码的 Spring MVC 端点。借助 Spring REST Docs,我们记录经过测试的行为,并作为测试周期的一部分重新生成片段,这有助于避免文档随时间推移而出现偏差。
我们创建了一个专用的表单模型,验证了输入字段,并公开了一个消费 application/x-www-form-urlencoded 请求的 POST 端点。最后,我们使用 formParameters() 来描述每个键,包括可选和忽略的字段。
我们涵盖了必需参数、可选参数、忽略的参数,以及严格模式和宽松模式之间的区别。此外,我们了解了如何定义 REST Docs 保存文档片段的位置。
一如既往,源代码可在 GitHub 上获取。
文章 使用 Spring REST Docs 记录表单参数 首次出现在 Baeldung 上。