HTTP 方法
GET、POST、PUT、PATCH 和 DELETE,以及其中哪些可以安全地重复。
方法是每个请求的第一个词。它告诉服务器你想对 URL 上的资源做什么。同一个 URL /api/Books/1登录后查看,用 GET 会返回一本书,用 PUT 或 PATCH 会修改它,用 DELETE 会删除它。大多数 API 用五个方法就能完成几乎所有事情,偶尔还会出现另外几个。
到了方法这一步,浏览器和测试工具的区别就开始显现了。地址栏只能发送 GET,其余的你需要 curl、Postman,或者像本页这样的“发送”按钮。修改共享数据的示例都使用 curl,这样本页的“发送”按钮就不会改动其他读者正在查看的记录。第 6 课用 curl 和 Postman 发送请求会把这两个工具都配置好。如果你还没有 curl,可以先在这里阅读 curl 示例,学完那一课后再运行。
五个日常方法
| 方法 | 作用 | 典型的成功状态码 |
|---|---|---|
GET | 读取一个资源或列表。不发送请求体。 | 200 |
POST | 创建一个新资源,或触发一个操作。 | 201,有时是 200 或 202 |
PUT | 用你发送的请求体替换一个资源。 | 200 或 204 |
PATCH | 修改资源的部分字段。 | 200 或 204 |
DELETE | 删除一个资源。 | 200 或 204 |
这个 API 的 Methods 端点各自只接受一个动词,并描述它们收到了什么。发送一个 GET:
/api/methods/get?x=1再发送一个带请求体的 POST:
/api/methods/post{"hello":"world"}这些端点不保存任何东西。要看方法对真实数据做了什么,你需要一个资源,本课剩下的部分使用 Books。
POST 用来创建
创建一本书:
curl -i -X POST https://api.testingapis.com/api/Books \
-H "Content-Type: application/json" \
-d '{"title":"The Pragmatic Programmer","author":"David Thomas",
"isbn":"9780135957059","genre":"programming","publishedYear":2019,
"pages":352,"language":"English","price":42.5,"rating":4.6,
"available":true,"tags":["career"]}'HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
{
"message": "Book created successfully",
"data": {
"id": 29,
"title": "The Pragmatic Programmer",
"author": "David Thomas",
"isbn": "9780135957059",
"genre": "programming",
"publishedYear": 2019,
"pages": 352,
"language": "English",
"price": 42.5,
"rating": 4.6,
"available": true,
"tags": ["career"]
}
}服务器返回了 201 Created,分配了 ID 29,并返回了新记录。你得到的 ID 会不一样,因为其他人也在创建书。这正是任何 POST 都要首先测试的一点:ID 由服务器选定,你的测试必须从响应中读取它,而不是去猜。
很多 API 在返回 201 时还会发送一个 Location 响应头,带上新资源的 URL,比如 Location: /api/Books/29登录后查看。这个 API 不发送。标准并不要求这样做,但如果文档承诺了,那就是一个 bug。
现在再运行一次同样的 POST。你会得到第二本书,ID 是新的,除了这个数字之外完全一样。POST 不能安全地重复:每次调用都会创建新东西。请记住这一点,后面的表格会用到。
PUT 替换,PATCH 修改
这两个方法都用来更新记录,人们经常把它们搞混。区别体现在你省略的字段上。
PATCH
只修改第 29 本书的价格:
curl -X PATCH https://api.testingapis.com/api/Books/29 \
-H "Content-Type: application/json" \
-d '{"price":39.99}'{
"message": "Book patched successfully",
"changed": ["price"],
"ignored": [],
"data": {
"id": 29,
"title": "The Pragmatic Programmer",
"author": "David Thomas",
"isbn": "9780135957059",
"genre": "programming",
"publishedYear": 2019,
"pages": 352,
"language": "English",
"price": 39.99,
"rating": 4.6,
"available": true,
"tags": ["career"]
}
}只有价格变了。你没有提到的字段都保持原值。这个 API 还会告诉你它修改了哪些字段,这很少见,对测试非常方便。
PUT
现在发送一个只有三个字段的 PUT:
curl -X PUT https://api.testingapis.com/api/Books/29 \
-H "Content-Type: application/json" \
-d '{"title":"The Pragmatic Programmer","author":"David Thomas","price":39.99}'{
"message": "Book updated successfully",
"data": {
"id": 29,
"title": "The Pragmatic Programmer",
"author": "David Thomas",
"isbn": "",
"genre": "",
"publishedYear": 0,
"pages": 0,
"language": "",
"price": 39.99,
"rating": 0,
"available": false,
"tags": []
}
}ISBN、类型、年份、页数、语言、评分和标签都没了。PUT 的意思是“这是这个资源完整的新版本”。你没有发送的字段都被替换成了空值或零。API 完全按照 PUT 的定义行事,却仍然因为客户端发送了不完整的请求体而抹掉了记录的大部分内容。
这是应用中最常见的真实 bug 之一。开发者用 PUT 更新一个字段,只发送了这个字段,结果记录上的其他字段都被悄悄清空了。测试更新功能时,发送一个不完整的请求体,然后 GET 这条记录,看看还剩下什么。
有些 API 的处理方式不同。它们会把缺少字段的 PUT 当作 PATCH 处理,或者因为缺少必填字段而用 400 拒绝它。最后一种可以说是最安全的。具体测试什么取决于文档,但一定要测。
DELETE 用来删除
删除第 29 本书,然后再删除一次:
$ curl -X DELETE https://api.testingapis.com/api/Books/29
{"message":"Book deleted successfully"} (200 OK)
$ curl -X DELETE https://api.testingapis.com/api/Books/29
{"status":404,"error":"Not Found","message":"Book with ID 29 does not exist."} (404 Not Found)第一次调用返回带消息的 200。第二次返回 404,因为已经没有东西可删了。删除之后,还要检查对同一个 ID 的 GET 是否返回 404。有些系统只是把记录标记为已删除,仍然继续返回它们,如果文档不是这么说的,那就是 bug。
安全方法和幂等方法
HTTP 标准中的两个特性,决定了客户端和测试可以如何对待每个方法。
安全的方法不会改变服务器上的任何东西,你想调用多少次都可以。幂等的方法可以改变东西,但调用两次对服务器的效果和调用一次相同。
| 方法 | 安全 | 幂等 |
|---|---|---|
GET | 是 | 是 |
HEAD | 是 | 是 |
OPTIONS | 是 | 是 |
PUT | 否 | 是 |
DELETE | 否 | 是 |
POST | 否 | 否 |
PATCH | 否 | 不保证 |
PUT 是幂等的,因为把同样完整的记录发送两次,留下的还是同一条记录。DELETE 是幂等的,尽管第二次调用得到了 404:响应不同,但服务器的状态(第 29 本书不存在)在两种情况下都一样。幂等性关乎效果,而不是响应。POST 不是幂等的,正如你看到的,第二次 POST 创建了第二本书。PATCH 两种情况都有可能。把价格设为 39.99 是幂等的;而“把库存数加 1”的 patch 就不是。
这很重要,因为网络会出故障。如果一个请求超时了,客户端并不知道服务器是否收到了它。重试 GET 或 PUT 没有坏处,重试 POST 却可能让一张卡被扣款两次。浏览器、代理和 HTTP 库都会根据这些规则来决定是否自行重试,超时、重试和限流会介绍 API 如何用幂等键让 POST 可以安全重试。
对测试人员来说,这些规则白送了你一批测试用例。调用一个 GET,检查什么都没变。调用两次 PUT,检查第二次调用没有创建或复制任何东西。会修改数据的 GET(比如 /api/deleteUser?id=5登录后查看,有些代码库里真有这样的东西)是设计上的 bug,因为爬虫、链接预览和浏览器预加载都会不经询问就发送 GET 请求。
HEAD 和 OPTIONS
HEAD 是不带响应体的 GET。服务器发送和 GET 相同的状态码与响应头,然后就此打住。客户端用它来检查某样东西是否存在,或者在不下载的情况下查看文件有多大。
HTTP/1.1 200 OK
x-message: HEAD request received. Responses to HEAD carry headers only.
x-method: HEADOPTIONS 询问某个 URL 上允许做什么。浏览器会在某些跨域请求之前自动发送它(称为“预检”),所以即使没有代码调用它,它也会出现在网络日志里。除非你负责 CORS 设置,否则很少需要直接测试它。
/api/methods/options使用错误的方法
/api/methods/get 只接受 GET。用 curl -i -X POST https://api.testingapis.com/api/methods/get 给它发一个 POST:
HTTP/1.1 405 Method Not Allowed
Content-Length: 0
allow: GET405 Method Not Allowed 表示这个 URL 存在,但不支持这个方法。allow 响应头列出了它支持的方法。对单本书发送 POST(这没有意义,因为书是在 /api/Books登录后查看 创建的)也会得到同样的待遇:
HTTP/1.1 405 Method Not Allowed
Content-Length: 0
allow: DELETE, GET, HEAD, PATCH, PUT向每个端点发送每种方法,是一个成本很低却能发现真实问题的测试。你要找的是本该被拒绝却没有被拒绝的方法(比如在一个本应只读的端点上能生效的 DELETE),以及本该是 405 的地方出现的 500 或 404。注意,这个 API 返回的 405 响应体是空的,而它的其他错误都是 JSON。期望每个 4xx 都带有 JSON 错误体的客户端会在这里栽跟头。
练习
用 curl 完成下面的练习(或者学完 curl 那一课后再回来做)。
- 用 POST 创建一本书,记下它的 ID。
- 用 PATCH 修改它的标题。GET 它,确认只有标题变了。
- 用一个包含除
tags之外所有字段的请求体对它执行 PUT。标签怎么样了? - 再发送一次同样的 PUT。第二次有什么变化吗?
- DELETE 它,GET 它,再 DELETE 一次。记下三个状态码。
- 向
/api/Books登录后查看(不带 ID)发送一个 DELETE。运行之前先预测状态码,然后查看allow响应头。