Ohhnews

分类导航

$ cd ..
Baeldung原文

使用Spring REST Docs记录表单参数

#spring rest docs#spring mvc#表单参数#api文档#参数校验

[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 注解来强制实施必需和受约束的值:

$ java
public class SubscriptionForm {
    @Email
    @NotBlank
    private String email;
    @NotBlank
    private String name;
    @NotBlank
    @Pattern(regexp = "weekly|monthly")
    private String frequency;
    private List<String> topics;
    private boolean marketingAccepted;
    // standard getters and setters
}

frequency 的模式明确指出了有效值。如果客户端发送的值不是 weekly 或 monthly,验证将在业务逻辑运行之前失败。请注意,Spring REST Docs 不会推断该约束,因此它不会自动将有效值添加到生成的文档中。

4. 测试设置和请求构建

REST Docs 的主要思想是从经过测试的行为中生成片段。因此,我们使用 MockMvc 和 REST Docs 配置来创建一个包含所有受支持表单字段的请求。

4.1. 测试设置和片段命名

我们的测试类使用 @WebMvcTest,我们还需要指定 RestDocumentationExtension,以便 JUnit 可以管理来自 Spring REST Docs 的 RestDocumentationContext:

$ java
@WebMvcTest
@ExtendWith(RestDocumentationExtension.class)
class NewsletterControllerDocumentationTest {
    @Autowired
    private MockMvc mockMvc;
    @BeforeEach
    void setUp(
      WebApplicationContext context, RestDocumentationContextProvider restDocumentation) {
        this.mockMvc = webAppContextSetup(context)
          .apply(documentationConfiguration(restDocumentation))
          .alwaysDo(document("{method-name}"))
          .build();
    }
}

我们的 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() 方法将键值对发送到接受表单负载的端点:

$ java
private MockHttpServletRequestBuilder postSubscription() {
    return post("/newsletter/subscriptions")
      .contentType(MediaType.APPLICATION_FORM_URLENCODED)
      .param("email", "api@baeldung.com")
      .param("name", "Baeldung API")
      .param("frequency", "weekly")
      .param("topics", "spring", "testing")
      .param("marketingAccepted", "true");
}

注意,我们使用 param("topics", "spring", "testing") 发送了两次 topics,这就是它处理多值表单字段的方式。

5. 记录表单参数

现在,我们准备好进行文档记录了。

5.1. 描述表单参数

我们使用 formParameters() 来记录表单负载。我们定义名称和描述,对于非必需参数使用 optional():

$ java
private FormParametersSnippet docFormParams() {
    return formParameters(
      parameterWithName("email").description("The subscriber email address"),
      parameterWithName("name").description("The display name of the subscriber"),
      parameterWithName("frequency").description("Delivery frequency: weekly or monthly"),
      parameterWithName("topics").optional()
        .description("One or more selected topic values"),
      parameterWithName("marketingAccepted").optional()
        .description("Whether marketing messages are accepted"));
}

如果有效的 frequency 值发生变化,我们需要记住手动更新参数描述,因为 @Pattern 注解不会推断它。

最重要的是,optional() 控制 Spring REST Docs 如何记录参数。它不会影响 Bean Validation。当 Spring 验证表单对象时,诸如 @NotBlank 之类的约束会独立应用。

如果我们添加或删除必需参数,通常会发生以下两种情况之一:要么请求验证行为改变并且测试失败,要么描述符匹配失败,因为我们记录的参数不再匹配请求。

5.2. 处理 Bean Validation 约束

为了避免手动描述约束,我们可以通过 ConstraintDescriptions 读取验证元数据,并通过调用 descriptionsForProperty() 将其合并到参数描述中。

让我们创建一个辅助方法来获取 SubscriptionForm 类和 frequency 属性的约束描述:

$ java
private String constraintsFor(String property) {
    ConstraintDescriptions constraints = new ConstraintDescriptions(SubscriptionForm.class);
    List<String> frequencyConstraints = constraints.descriptionsForProperty("frequency");
}

现在,我们可以回到 docFormParams() 并使用我们的 constraintsFor() 方法:

$ java
parameterWithName("frequency")
  .description("Delivery frequency. Constraints: " + constraintsFor("frequency")),

这是我们得到的描述:

$ java
Delivery frequency. Constraints: Must match the regular expression `weekly|monthly`, Must not be blank

因此,它包含了所有验证注解的描述。如果它们发生变化,描述也会随之变化。

5.3. 定义被忽略的参数

一些客户端发送额外的表单值,我们不想将其包含在 API 文档中。例如,前端跟踪参数与业务逻辑无关。默认情况下,如果 REST Docs 发现任何未记录的参数,它会失败并抛出 SnippetException,因此我们必须显式地忽略它们。

让我们首先回到 postSubscription() 并发送我们稍后将忽略的 trackingId 参数:

$ java
private MockHttpServletRequestBuilder postSubscription() {
    return post("/newsletter/subscriptions")
      // previous parameters ...
      .param("trackingId", "campaign-42");
}

现在,在 docFormParams() 中,我们将 trackingId 添加为被忽略的参数:

$ java
private FormParametersSnippet docFormParams() {
    return formParameters(
      // earlier parameters ...
      parameterWithName("trackingId").ignored());
}

这样,该参数可以出现,但它不是我们最终获得的 API 文档的一部分。

6. 执行测试并生成片段

最后,让我们将所有这些部分放在一个模拟请求中:

$ java
@Test
void whenFormRequestIsValid_thenDocumentFormParameters() throws Exception {
    mockMvc.perform(postSubscription())
      .andExpect(status().isCreated())
      .andExpect(jsonPath("$.id").isNumber())
      .andDo(
        document("newsletter-subscribe", 
          docFormParams()));
}

这个单一的测试验证了端点,并为所有表单参数定义了字段级文档。最重要的是,它有助于将契约保持在一个地方。

当我们运行已记录的测试时,Spring REST Docs 会在 target/generated-snippets/newsletter-subscribe 下写入片段文件,这是我们使用 document() 方法选择的名称。

6.1. 生成的表单片段

为我们的表单 POST 测试执行生成的片段以结构化格式显示参数,使用我们通过 parameterWithName() 调用提供的描述:

$ md
|===
|Parameter|Description
|`+email+`
|The subscriber email address
|`+name+`
|The display name of the subscriber
|`+frequency+`
|Delivery frequency: weekly or monthly
|`+topics+`
|One or more selected topic values
|`+marketingAccepted+`
|Whether marketing messages are accepted
|===

我们可以在 Maven 构建期间使用 Asciidoctor 插件将此片段包含在 AsciiDoc 页面中。

主要优势在于,我们的片段来自实际在测试中运行的请求。如果测试的行为或描述符不再匹配,测试或片段生成就会失败,迫使我们一起更新代码和文档。

7. 严格模式与宽松模式的参数文档

默认情况下,formParameters(...) 是严格的。这意味着未记录的参数会导致片段失败,除非我们将它们标记为被忽略。

然而,有时我们想要更宽松的约束。例如,我们可能只关心记录核心字段,同时允许请求中包含额外的键。 在这种情况下,我们通过 relaxedFormParameters() 而不是 formParameters() 进入宽松模式。 让我们用 relaxedFormParameters() 编写一个新的辅助方法:

$ java
private FormParametersSnippet docRelaxedCoreFormParams() {
    return relaxedFormParameters(
      parameterWithName("email").description("The subscriber email address"),
      parameterWithName("name").description("The display name of the subscriber"),
      parameterWithName("frequency").description("Delivery frequency: weekly or monthly"));
}

以下是配套的测试方法:

$ java
@Test
void whenOnlyCoreFieldsMatter_thenDocumentWithRelaxedMode() throws Exception {
    mockMvc.perform(postSubscription())
      .andExpect(status().isCreated())
      .andDo(document("newsletter-subscribe-relaxed", docRelaxedCoreFormParams()));
}

它提交表单,检查返回状态是否为 created,然后在 newsletter-subscribe-relaxed 文件夹中生成文档片段。

7.1. 参数模式快速参考

让我们回顾所有参数处理模式:

模式如何使用会发生什么
必需参数parameterWithName("email")必须存在于记录的请求中
可选参数parameterWithName("topics").optional()我们可以省略它而不会导致片段失败
忽略的参数parameterWithName("trackingId").ignored()可以存在,但被排除在发布的契约之外
宽松模式relaxedFormParameters(...)容忍未记录的参数

在实践中,我们将对稳定的契约使用严格模式,为已知的非契约输入添加 ignored(),并且仅在我们需要额外灵活性时切换到宽松模式。

8. 结论

在本文中,我们使用 Spring REST Docs 和测试优先的工作流程记录了一个表单编码的 Spring MVC 端点。借助 Spring REST Docs,我们记录经过测试的行为,并作为测试周期的一部分重新生成片段,这有助于避免文档随时间推移而出现偏差。

我们创建了一个专用的表单模型,验证了输入字段,并公开了一个消费 application/x-www-form-urlencoded 请求的 POST 端点。最后,我们使用 formParameters() 来描述每个键,包括可选和忽略的字段。

我们涵盖了必需参数、可选参数、忽略的参数,以及严格模式和宽松模式之间的区别。此外,我们了解了如何定义 REST Docs 保存文档片段的位置。

一如既往,源代码可在 GitHub 上获取。

文章 使用 Spring REST Docs 记录表单参数 首次出现在 Baeldung 上。