用 curl 和 Postman 发送请求
你每天都会用到的两个工具,以及何时该选哪一个。
到目前为止,你都是从浏览器和这些页面上的“发送”按钮发出请求的。这只够用来发送简单的 GET 请求。要选择方法、设置请求头或发送请求体,你需要一个真正的客户端。几乎每个做 API 测试的人最后都会用两个:命令行里的 curl,以及像 Postman 这样的图形化客户端。这一课会把两者各讲到足够你完成本教程的剩余部分。
curl
curl 是一个命令行程序,它发送请求并打印响应。macOS、大多数 Linux 系统以及 Windows 10 和 11 都默认安装了它。用 curl --version 检查一下。它也是开发者分享请求时使用的格式:API 文档、浏览器开发者工具和 Postman 都能把请求显示或导出为 curl 命令,所以即使你更喜欢别的工具,也需要能读懂它们。
不加任何选项时,curl 只打印响应体。加上 -i 就会同时显示状态行和响应头,测试时你几乎总是需要它们:
curl -i https://api.testingapis.com/api/healthHTTP/1.1 200 OK
content-type: application/json; charset=utf-8
date: Sun, 11 Oct 2026 11:47:45 GMT
server: Kestrel
x-powered-by: TestingAPIs
transfer-encoding: chunked
{"status":"healthy","uptimeSeconds":76890,"timestamp":"2026-10-11T11:47:46Z"}想看更多细节,-v(详细模式)还会打印 curl 发送的请求(用 > 标记)和响应头(用 < 标记)。以 * 开头的行描述的是连接。当请求的行为不符合预期,而你想准确看到网络上传输了什么时,就用它。
curl -v https://api.testingapis.com/api/health* Host api.testingapis.com:443 was resolved.
* IPv4: 135.13.233.203
* Established connection to api.testingapis.com (135.13.233.203 port 443)
* using HTTP/1.x
> GET /api/health HTTP/1.1
> Host: api.testingapis.com
> User-Agent: curl/8.18.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< content-type: application/json; charset=utf-8
< date: Sun, 11 Oct 2026 11:49:47 GMT
< transfer-encoding: chunked
...curl 添加了三个你没有要求的请求头:Host、User-Agent 和 Accept。每个客户端都会添加一些默认请求头,而不同的客户端添加的各不相同。当同一个请求在 Postman 里能用、在 curl 里却失败时,原因往往就是某个默认请求头。
方法、请求头和请求体
三个选项就能覆盖大多数请求。-X 设置方法。-H 添加一个请求头,可以重复使用。-d 发送请求体。下面向一个会原样返回所收内容的端点发送一个带 JSON 请求体的 POST:
curl -X POST https://api.testingapis.com/api/methods/post \
-H "Content-Type: application/json" \
-d '{"name":"Asha","role":"tester"}'{"method":"POST","message":"POST request received","path":"/api/methods/post","query":{},"contentType":"application/json","bodyLength":31,"bodyTruncated":false,"body":"{\"name\":\"Asha\",\"role\":\"tester\"}","timestamp":"2026-09-30T18:31:16Z"}每行末尾的反斜杠让命令在 bash 和 zsh 中延续到下一行。请求体用单引号括起来,这样 shell 就不会动里面的双引号。如果你忘了 Content-Type 请求头,curl 会发送 application/x-www-form-urlencoded,因为 -d 本来是为 HTML 表单设计的,很多 API 会拒绝这样的请求体。
curl 7.82 及以后的版本有一个快捷方式 --json,它会发送请求体,设置 Content-Type: application/json 和 Accept: application/json,并把方法切换为 POST。下面这条命令会创建一个真实的待办:
curl --json '{"title":"Learn curl","completed":false,"userId":1}' \
https://api.testingapis.com/api/Todos{"message":"Todo created successfully","data":{"id":31,"userId":1,"title":"Learn curl","completed":false,"priority":"medium","dueDate":""}}你拿到的 ID 会不一样,因为其他人也在创建待办。对于 PUT 或 PATCH,在 --json 旁边加上 -X PUT 或 -X PATCH。
计时和脚本
-w 会在响应之后打印 curl 测量到的数值。配合 -s(静默,不显示进度条)和 -o /dev/null(丢弃响应体),就能得到一行摘要:
curl -s -o /dev/null \
-w "status %{http_code} total %{time_total}s size %{size_download} bytes\n" \
https://api.testingapis.com/api/Productsstatus 200 total 0.382512s size 5846 bytes在 Windows 上,用 -o NUL 代替 -o /dev/null。time_total 包含 DNS 查询、建立连接和 TLS 的时间,所以第一个请求往往比后面的慢。性能测试那一课会进一步拆解这个数字。
在 Windows 上使用 curl
Windows 需要单独一节,因为同一条命令在不同的 shell 中可能有三种不同的表现。
在 Windows PowerShell 5.1(Windows 自带的版本)中,curl 并不是 curl。它是 Invoke-WebRequest 的别名,这是一个选项完全不同的 PowerShell 命令,所以 curl -i 会报出一个令人费解的错误。输入 curl.exe 才能运行真正的程序。PowerShell 7 去掉了这个别名,但输入 curl.exe 在两者中都能用,所以这是个好习惯。
引号是第二个问题。Windows PowerShell 5.1 把参数传给 curl.exe 这类程序时,会去掉参数中的双引号。下面的命令看起来没问题,服务器收到的却是损坏的 JSON:
PS> curl.exe -s -X POST https://api.testingapis.com/api/methods/post -H "Content-Type: application/json" -d '{"name":"Asha"}'
{"method":"POST", ... "bodyLength":11, ... "body":"{name:Asha}", ...}请求体到达时变成了 {name:Asha},这不是 JSON。PowerShell 7.3 及以后的版本能正确传递引号。在更早的版本上,最省事的办法是把请求体放进一个文件,然后用 @ 让 curl 读取它。"@body.json" 外面的引号是为了防止 PowerShell 把 @ 当成它自己的语法。
# body.json contains {"name":"Asha","role":"tester"}
curl.exe -s --json "@body.json" https://api.testingapis.com/api/methods/post在老式的命令提示符(cmd.exe)中,单引号没有任何意义,所以要用双引号括住请求体,并用反斜杠转义里面的双引号。续行符是 ^ 而不是反斜杠:
curl -X POST https://api.testingapis.com/api/methods/post ^
-H "Content-Type: application/json" ^
-d "{\"name\":\"Asha\",\"role\":\"tester\"}"Postman 和其他图形化客户端
图形化客户端做的事和 curl 一样,只是用表单代替了命令:从下拉框选择方法,输入 URL,在各个标签页中填写请求头和请求体,然后点击 Send。响应会显示在下方,经过格式化,并附带状态码、耗时和大小。Postman 用得最广。Insomnia 和 Bruno 的用法相同,而 Bruno 把所有内容都存成普通文件,有些团队更喜欢这样,因为请求可以和代码放在同一个 Git 仓库里。下面的概念适用于所有这些工具,名称以 Postman 为准。
集合
集合是一组保存起来的请求,通常每个 API 一个,里面还可以有文件夹。对于一个你要做好几个月的项目,这正是选择图形化客户端而不是 curl 的理由:请求只需搭建一次,然后把集合分享给团队。
你不必手动搭建。这个 API 在 https://api.testingapis.com/openapi/v1.json登录后查看 发布了一份 OpenAPI 文档,它以机器可读的方式描述了每个端点。在 Postman 中选择 Import,粘贴这个 URL,它就会创建一个为每个端点各准备一个请求的集合。你在工作中测试的大多数 API 都会有类似的文件,这是上手一个陌生 API 最快的方式。
环境和变量
真实的项目会在多个地方运行同一个 API:开发者的笔记本、测试服务器、预发布环境、生产环境。除了基础 URL 和凭据之外,请求都是一样的。Postman 用环境来处理这个问题。创建一个名为“testingapis”的环境,其中变量 baseUrl 设为 https://api.testingapis.com,然后把请求的 URL 写成 {{baseUrl}}/api/Products/1。切换环境就会一次性切换所有请求。令牌也放进变量里,这样就不会被复制到几十个请求中。
Postman 中的测试
每个请求都有一个 Tests 标签页(在较新的版本中叫 Scripts,里面有 post-response 部分),你可以在其中编写在响应到达后运行的 JavaScript。对于 GET {{baseUrl}}/api/Products/1:
pm.test("status is 200", function () {
pm.response.to.have.status(200);
});
pm.test("product 1 has a price", function () {
const product = pm.response.json();
pm.expect(product.id).to.eql(1);
pm.expect(product.price).to.be.a("number");
});结果会显示在响应旁边的 Test Results 标签页中。之后,Collection Runner 可以运行集合中的每个请求,并报告哪些检查失败了。这是迈向自动化的平缓第一步。自动化那一课会讲如何把测试写成普通代码,当测试数量超过几十个时,这种方式扩展性更好。
该用哪一个
两个都用,各司其职。
| 场景 | 更好的选择 |
|---|---|
| 快速检查一件事,或复现一份 bug 报告 | curl |
| 在工单或聊天中分享一个精确的请求 | curl(任何人都能粘贴运行) |
| 探索一个你还不熟悉的 API | 导入 OpenAPI 的图形化客户端 |
| 需要反复编辑的长 JSON 请求体 | 图形化客户端 |
| 在测试服务器和预发布服务器之间切换 | 图形化客户端的环境 |
| 在脚本或构建服务器上运行 | curl,或用代码编写的测试 |
| 查看原始请求头和连接细节 | curl -v |
针对这个 API 还有一个选择:API 参考文档中每个端点的页面都有一个用来发送请求的表单,并显示一条可以直接复制的 curl 命令。在把端点保存到任何地方之前,这往往是试用它最快的方式。
- 当 bug 报告中包含请求时,把它作为 curl 命令附上。开发者无需导入任何东西就能运行它,而且发送了哪些请求头也一目了然。
- Postman 可以双向转换。把 curl 命令粘贴到 Import 对话框中就能得到一个请求,或者使用请求上的 Code 按钮得到 curl。
练习
先用 curl 完成下面的练习,然后在图形化客户端中再做一遍。
- 发送
GET /api/echo/headers,比较 curl 发送的请求头和你的图形化客户端发送的请求头。每个工具各自添加了哪些请求头? - 用
POST /api/Todos创建一个待办,然后用GET /api/Todos/加上响应中的 ID 来获取它。 - 发送同样的 POST,但不带
Content-Type请求头。返回了什么状态码和响应体? - 用
-w给/api/Products和/api/Products?delay=2计时。delay参数会让服务器等待,所以第二个应该多花大约两秒。
在你的图形化客户端中,创建一个带 baseUrl 变量的环境,并写一个故意失败的测试,这样在依赖测试通过之前,你就知道失败是什么样子。