编写你的第一批测试用例

预期结果、反向测试、边界值和等价类。

到目前为止,你一直在发送请求并查看返回的内容。当你在发送之前就决定应该返回什么,然后再去比较时,测试才真正开始。把这个决定写下来,就是一个测试用例。这一课讲如何编写测试用例,以及如何选择要写哪些,因为你不可能把所有可能的请求都发一遍。

测试用例包含什么

不同团队使用的模板不同,但几乎都包含同样的几个部分:

部分写什么示例
ID一个简短的编号,方便大家讨论PROD-GET-03
标题用一句话说明检查的内容未知的商品 ID 返回 404
前置条件开始之前必须满足的条件不存在 ID 为 9999 的商品
请求方法、URL、请求头和请求体GET /api/Products/9999
预期结果状态码、重要的响应头、响应体404,JSON 响应体的消息中提到 ID 9999
实际结果执行时填写符合预期

预期结果是人们最容易写不好的部分。“返回一个错误”不算预期结果,因为几乎什么结果都能满足它。要具体写明状态码、重要的响应头(通常是 Content-Type,有时是缓存或 Location 响应头)以及响应体:哪些字段必须存在,类型和值分别是什么。如果你说不出正确答案是什么,那你就找到了一个该问开发者或产品负责人的问题,值得在测试任何东西之前先问清楚。

预期结果从哪里来?来自文档、描述该功能的工单、同一 API 中类似端点的行为,有时也来自常识(价格不应该是负数)。当这些来源互相矛盾时,写下你遵循的是哪一个。

正向测试和反向测试

正向测试发送一个有效的请求,检查它是否正常工作:商品 1 存在,所以 GET /api/Products/1 会返回它。反向测试发送一些 API 应该拒绝的内容,检查它是否正确地拒绝:正确的状态码、有用的消息,并且没有副作用。

初学者大多写正向测试,因为他们照着功能描述来写。开发者在交付之前通常已经亲自试过正向路径了,而问题往往是在反向测试中发现的。粗略地说,对于任何接收输入的端点,反向测试的数量应该多于正向测试。

等价类

列表端点 GET /api/Products 接受一个 limit 参数。API 参考文档说它表示每页大小,范围是 1 到 100。你可以发送的值有无穷多个。等价类划分的意思是,把服务器应该同样对待的输入分成一组,每组只测一个值,而不是全部都测:

类别示例值预期
有效,在范围内limit=10十个商品
低于范围limit=-5被拒绝,或被调整为最小值
高于范围limit=500被拒绝,或被调整为 100
不是数字limit=abc以 400 拒绝
不是整数limit=2.5以 400 拒绝
空值limit=按未提供处理
未提供没有 limit按文档所述,返回所有商品

注意那些“被拒绝,或被调整”的条目。文档没有说明超出范围时会发生什么,所以存在两种合理的行为,两种都说得过去。你要测试的是 API 始终如一地采用其中一种,并且不会崩溃,也不会返回莫名其妙的结果。

边界值

bug 往往集中在范围的边缘,因为开发者正是在这里把本该写 <= 的地方写成了 <。边界值分析的意思是测试边界本身,以及边界两侧各一步的值。对于 1 到 100 的范围,就是 0、1、2、99、100 和 101。下面是这个 API 对这些值以及其他类别的实际处理方式。X-Per-Page 响应头报告服务器实际使用的每页大小。

请求状态码返回的条目数X-Per-Page
?limit=020011
?limit=120011
?limit=-120011
?limit=10020020(全部)100
?limit=10120020(全部)100
?limit=abc2002020
?limit=2.52002020
?limit=2002020
GET/api/Products?limit=0

解读结果:小于 1 的值被调整为 1,大于 100 的值被调整为 100,任何非整数的值都被忽略,就像没有提供 limit 一样。这些都没有写在文档里,但行为是一致的,所以很可能是有意为之。不过有两点仍然值得在测试报告中记上一笔。limit=0 返回一个条目令人意外:一个请求零个条目的客户端(比如想低成本地读取总数)却收到了一个它不想要的商品。而 limit=abc 被悄悄忽略,意味着客户端里的拼写错误不会被发现,而返回 400 本可以抓住它。

这里还有一个关于测试数据的教训。这个 API 只有 20 个商品,所以 limit=100 和 limit=101 返回的响应体完全相同。仅凭响应体你无法看出 101 被调整过,是响应头揭示了这一点。当边界比你的数据量还大时,要么像这样找另一个信号,要么创建足够多的记录来触及边界。

边界也藏在 ID 里

参考文档把 /api/Products/{id} 中的 id 列为 int32,即最大值为 2,147,483,647 的 32 位整数。这是一个值得测试的边界:

请求状态码响应体
/api/Products/20200商品 20,最后一个
/api/Products/21404JSON 错误消息
/api/Products/0404JSON 错误消息
/api/Products/-1404JSON 错误消息
/api/Products/2147483647404JSON 错误消息
/api/Products/2147483648404空
/api/Products/abc404空
/api/Products/1.5404空
/api/Products/01200商品 1

有两点很突出。所有不是有效 int32 的值都得到一个完全没有响应体的 404,而有效但不存在的 ID 得到的是 JSON 错误。另外,01 被当作 1 接受了。后者在这里无害,但在按 URL 缓存响应的 API 上,/1 和 /01 会被分别缓存。两个结果都不算明显错误,但都值得了解。

一个端点的一组测试用例

综合起来,GET /api/Products/{id} 的一组合理的初始测试用例如下。这并不是你能测试的全部内容,但它覆盖了每个类别各一次,以及每个边界。

ID请求预期结果
PROD-GET-01GET /api/Products/1200,JSON,id 为 1,title 是非空字符串,price 是大于 0 的数字
PROD-GET-02GET /api/Products/20200,初始数据中最大的 ID
PROD-GET-03GET /api/Products/21404,JSON 错误响应体中提到 ID 21
PROD-GET-04GET /api/Products/0404,JSON 错误响应体
PROD-GET-05GET /api/Products/-1404,JSON 错误响应体
PROD-GET-06GET /api/Products/abc400 或 404,带 JSON 错误响应体
PROD-GET-07GET /api/Products/2147483648400 或 404,带 JSON 错误响应体
PROD-GET-08HEAD /api/Products/1与 GET 相同的状态码和响应头,没有响应体
PROD-GET-09GET /api/Products/1 两次两次的响应体完全相同

在当前的 API 上,PROD-GET-06 和 07 会失败,因为响应体是空的。PROD-GET-08 也会失败:HEAD /api/Products/1 返回 405 Method Not Allowed,而像 /api/Books/{id}登录后查看 这样较新的资源支持 HEAD。这里是否应该支持 HEAD,是要问 API 负责人的问题。这没关系。测试用例记录的是应该发生什么;失败的测试用例正是你发现它没有发生的方式。PROD-GET-02 有一个弱点:如果有人删除了商品 20 或添加了商品 21,它就不再代表它所说的含义了。下一课会介绍如何创建你自己的测试数据,让测试不依赖别人控制的记录。

撰写 bug 报告

测试用例失败时,你要把它写成报告。好的 bug 报告能让开发者不用问你任何问题,就在一分钟内复现问题。以上面那些空的 404 为例,先比较这两个响应:

GET /api/Products/9999
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8

{"status":404,"error":"Not Found","message":"Product with ID 9999 does not exist."}
GET /api/Products/abc
HTTP/1.1 404 Not Found
Content-Length: 0

一份报告可以这样写(这里用的是英文,许多团队都这样写):

bug 报告
Title: GET /api/Products/{id} returns an empty 404 for non-numeric
       and out-of-range IDs, instead of the JSON error body

Environment: https://api.testingapis.com, 30 Sep 2026

Steps:
  curl -i https://api.testingapis.com/api/Products/9999
  curl -i https://api.testingapis.com/api/Products/abc
  curl -i https://api.testingapis.com/api/Products/2147483648

Expected:
  All three return 404 with Content-Type: application/json and a body
  like {"status":404,"error":"Not Found","message":"..."}, as documented
  under "Responses and errors" in the API reference.

Actual:
  9999        404, JSON body with a message
  abc         404, Content-Length: 0, no Content-Type, no body
  2147483648  404, empty body (one above the int32 maximum; 2147483647
              returns the JSON error)

Impact:
  Clients that parse every error body as JSON fail on the empty
  responses. The empty 404 also looks like "this URL does not exist"
  rather than "this product does not exist".

Notes:
  Probably the route only matches int32 values, so other input never
  reaches the controller. Could be intended; if so, the docs should say
  which errors have no body.

这份报告有用的原因:

  • 标题说明了哪里出了什么问题,所以在五十个工单的列表里也一目了然。
  • 步骤是可以直接粘贴的命令,而不是“用一个错误的 ID 调用商品端点”。
  • 预期行为指向了一个来源(文档),所以不只是你的个人偏好。
  • 它展示的是模式,而不是单个例子。三个请求,其中一个正常工作用来对照,比一个失败的请求更有说服力。
  • 它说明了对真实客户端的影响。开发者按影响来排优先级,光说“不一致”很少会被修复。
  • 它承认了不确定性。这种行为可能是有意的,报告说明了两种情况下怎样解决(修改代码,或者修改文档)。

练习

选择 GET /api/Books/{id}登录后查看 或 GET /api/Countries/code/{code}登录后查看。在 API 参考文档中查到这个端点,然后:

  • 列出路径参数的等价类。
  • 挑选边界值。对于国家代码,考虑长度和大小写。
  • 按上面的表格格式写出至少八个测试用例,并给出具体的预期结果。
  • 用 curl 运行它们,并填写实际结果。

如果有任何行为与你的预期不同,就按上面的结构写一份 bug 报告,包括你不确定的地方。

练习与延伸