强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

curl 深度教程 / 第 13 章:API 测试

第 13 章:API 测试

curl 是最轻量级的 API 测试工具——无需安装额外软件,一个命令就能完成从简单的 GET 到复杂的 GraphQL 查询。


13.1 REST API 测试

CRUD 全流程

# CREATE - 创建资源
curl -s -X POST https://api.example.com/users \
  -H "Content-Type: application/json" \
  -d '{"name": "张三", "email": "zhangsan@example.com"}' \
  | jq .
# 响应:{"id": 1, "name": "张三", "email": "zhangsan@example.com"}

# READ - 读取资源
curl -s https://api.example.com/users/1 | jq .

# UPDATE (PUT) - 完整更新
curl -s -X PUT https://api.example.com/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name": "张三(已更新)", "email": "new@example.com"}' \
  | jq .

# UPDATE (PATCH) - 部分更新
curl -s -X PATCH https://api.example.com/users/1 \
  -H "Content-Type: application/json" \
  -d '{"email": "updated@example.com"}' \
  | jq .

# DELETE - 删除资源
curl -s -X DELETE https://api.example.com/users/1 \
  -w "HTTP 状态码: %{http_code}\n"

# LIST - 列表查询(带分页和过滤)
curl -s "https://api.example.com/users?page=1&per_page=20&sort=name" | jq .

httpbin.org 实战

httpbin 是一个优秀的 HTTP 测试服务,返回你发送的请求信息:

# 测试 GET 请求
curl -s "https://httpbin.org/get?name=curl&version=8" | jq '.args'

# 测试 POST JSON
curl -s -X POST https://httpbin.org/post \
  -H "Content-Type: application/json" \
  -d '{"test": true}' | jq '.json'

# 测试自定义头部
curl -s -H "X-Custom-Header: test-value" \
  -H "Accept-Language: zh-CN" \
  https://httpbin.org/headers | jq '.headers'

# 测试 Cookie
curl -s -b "session=abc123; user=admin" \
  https://httpbin.org/cookies | jq .

# 测试 Basic Auth
curl -s -u admin:secret https://httpbin.org/basic-auth/admin/secret | jq .

# 测试 Bearer Token
curl -s -H "Authorization: Bearer test-token" \
  https://httpbin.org/bearer | jq .

# 测试重定向
curl -s -L -o /dev/null -w "重定向次数: %{num_redirects}\n最终 URL: %{url_effective}\n" \
  https://httpbin.org/redirect/3

# 测试延迟
curl -s --max-time 6 "https://httpbin.org/delay/3" | jq '.url'

# 测试状态码
for code in 200 201 301 400 401 403 404 500 502 503; do
  status=$(curl -s -o /dev/null -w "%{http_code}" "https://httpbin.org/status/$code")
  echo "请求 $code → 返回 $status"
done

# 测试文件上传
curl -s -X POST https://httpbin.org/post \
  -F "file=@/etc/hostname;type=text/plain" \
  -F "description=测试文件" | jq '.files'

13.2 GraphQL 测试

基本 GraphQL 查询

# GraphQL 查询(使用 POST + JSON)
curl -s -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -d '{
    "query": "{ users { id name email } }"
  }' | jq '.data.users'

# 带变量的查询
curl -s -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -d '{
    "query": "query GetUser($id: ID!) { user(id: $id) { id name email role } }",
    "variables": { "id": "42" }
  }' | jq '.data.user'

# Mutation 操作
curl -s -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "query": "mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id name } }",
    "variables": {
      "input": {
        "name": "张三",
        "email": "zhangsan@example.com",
        "role": "ADMIN"
      }
    }
  }' | jq '.data.createUser'

GraphQL 批量查询

# 在单个请求中执行多个操作
curl -s -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -d '[
    { "query": "{ users { id name } }" },
    { "query": "{ posts { id title } }" },
    { "query": "{ stats { totalUsers totalPosts } }" }
  ]' | jq .

# 从文件执行查询
cat << 'EOF' > query.graphql
query SearchUsers($keyword: String!, $limit: Int) {
  searchUsers(keyword: $keyword, limit: $limit) {
    id
    name
    email
    createdAt
  }
}
EOF

VARIABLES=$(jq -n --arg keyword "张三" --argjson limit 10 \
  '{keyword: $keyword, limit: $limit}')

jq -n --rawfile query query.graphql --argjson variables "$VARIABLES" \
  '{query: $query, variables: $variables}' | \
  curl -s -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -d @- | jq '.data.searchUsers'

GraphQL Schema 自省

# 查询 Schema 中的所有类型
curl -s -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ __schema { types { name kind } } }"}' \
  | jq '.data.__schema.types[] | select(.kind == "OBJECT") | .name'

# 查询特定类型的字段
curl -s -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ __type(name: \"User\") { fields { name type { name } } } }"}' \
  | jq '.data.__type.fields[]'

# 查询可用的查询和变更
curl -s -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ __schema { queryType { fields { name } } mutationType { fields { name } } } }"}' \
  | jq '.data.__schema'

13.3 gRPC 测试

gRPC 使用 HTTP/2 和 Protocol Buffers,curl 支持 gRPC-Web 和部分原生 gRPC。

gRPC-Web 测试

# gRPC-Web 使用 HTTP/1.1 或 HTTP/2,Content-Type 为 application/grpc-web
# 需要 gRPC-Web 代理(如 Envoy)将请求转换为原生 gRPC

# 发送 gRPC-Web 请求
curl -s -X POST http://localhost:8080/package.Service/Method \
  -H "Content-Type: application/grpc-web" \
  -H "X-Grpc-Web: 1" \
  --data-binary @request.bin

# 使用 grpcurl 替代(推荐用于原生 gRPC 测试)
# grpcurl -plaintext localhost:50051 package.Service/Method

使用 curl 测试 gRPC 健康检查

# gRPC 健康检查协议(标准)
# 需要发送 protobuf 编码的请求
# HealthCheck 的 protobuf 消息:空消息 = "\x00\x00\x00\x00\x00"

# 简单的 gRPC 端点可达性测试
curl -s --http2 \
  -X POST "http://localhost:50051/grpc.health.v1.Health/Check" \
  -H "Content-Type: application/grpc" \
  -H "TE: trailers" \
  --data-binary $'\x00\x00\x00\x00\x00' \
  -w "HTTP 状态码: %{http_code}\n"

# 建议使用专门的 gRPC 工具
# grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check

13.4 自动化测试脚本

REST API 测试框架

#!/bin/bash
# api_test.sh - 简单的 REST API 测试框架

BASE_URL="${1:-http://localhost:8080}"
PASSED=0
FAILED=0

# 断言函数
assert_status() {
  local desc="$1" url="$2" expected="$3" method="${4:-GET}" data="$5"
  
  local curl_args=(-s -o /dev/null -w "%{http_code}" -X "$method")
  [[ -n "$data" ]] && curl_args+=(-H "Content-Type: application/json" -d "$data")
  
  local actual
  actual=$(curl "${curl_args[@]}" "$url")
  
  if [ "$actual" = "$expected" ]; then
    echo "✅ PASS: $desc (HTTP $actual)"
    ((PASSED++))
  else
    echo "❌ FAIL: $desc (expected $expected, got $actual)"
    ((FAILED++))
  fi
}

assert_json() {
  local desc="$1" url="$2" jq_expr="$3" expected="$4"
  
  local actual
  actual=$(curl -s "$url" | jq -r "$jq_expr" 2>/dev/null)
  
  if [ "$actual" = "$expected" ]; then
    echo "✅ PASS: $desc ('$actual')"
    ((PASSED++))
  else
    echo "❌ FAIL: $desc (expected '$expected', got '$actual')"
    ((FAILED++))
  fi
}

assert_header() {
  local desc="$1" url="$2" header="$3" expected="$4"
  
  local actual
  actual=$(curl -sI "$url" | grep -i "^$header:" | awk '{print $2}' | tr -d '\r')
  
  if echo "$actual" | grep -qi "$expected"; then
    echo "✅ PASS: $desc ($header: $actual)"
    ((PASSED++))
  else
    echo "❌ FAIL: $desc (expected '$expected', got '$actual')"
    ((FAILED++))
  fi
}

# 测试用例
echo "=== API 测试 ($BASE_URL) ==="

assert_status "GET /health 返回 200" "$BASE_URL/health" "200"
assert_status "GET /users 返回 200" "$BASE_URL/users" "200"
assert_status "GET /nonexistent 返回 404" "$BASE_URL/nonexistent" "404"
assert_status "POST /users 创建成功" "$BASE_URL/users" "201" "POST" \
  '{"name": "测试用户", "email": "test@example.com"}'
assert_status "POST /users 无 body 返回 400" "$BASE_URL/users" "400" "POST" ""
assert_header "响应包含 JSON Content-Type" "$BASE_URL/users" "content-type" "application/json"

echo "---"
echo "通过: $PASSED, 失败: $FAILED"
[ "$FAILED" -eq 0 ] && exit 0 || exit 1

带 Token 的 API 测试

#!/bin/bash
# auth_api_test.sh - 带认证的 API 测试

BASE_URL="https://api.example.com"

# 1. 获取 Token
TOKEN=$(curl -sS -X POST "$BASE_URL/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"username": "testuser", "password": "testpass"}' \
  | jq -r '.access_token')

if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
  echo "❌ 获取 Token 失败"
  exit 1
fi
echo "✅ 获取 Token 成功"

# 2. 使用 Token 测试 API
AUTH="-H \"Authorization: Bearer $TOKEN\""

# 测试获取用户列表
HTTP_CODE=$(curl -sS -o /tmp/users.json -w "%{http_code}" \
  -H "Authorization: Bearer $TOKEN" \
  "$BASE_URL/users")

if [ "$HTTP_CODE" = "200" ]; then
  COUNT=$(jq 'length' /tmp/users.json)
  echo "✅ 获取用户列表成功 ($COUNT 条记录)"
else
  echo "❌ 获取用户列表失败 (HTTP $HTTP_CODE)"
fi

# 测试无 Token 访问(应返回 401)
HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" "$BASE_URL/users")
if [ "$HTTP_CODE" = "401" ]; then
  echo "✅ 无 Token 访问正确返回 401"
else
  echo "❌ 无 Token 访问应返回 401,实际返回 $HTTP_CODE"
fi

响应断言详解

# 断言 JSON 字段值
assert_field() {
  local url="$1" field="$2" expected="$3"
  local actual=$(curl -s "$url" | jq -r "$field")
  if [ "$actual" = "$expected" ]; then
    echo "✅ $field = $expected"
  else
    echo "❌ $field: expected '$expected', got '$actual'"
    return 1
  fi
}

# 断言 JSON 包含特定数组长度
assert_array_length() {
  local url="$1" field="$2" expected="$3"
  local actual=$(curl -s "$url" | jq "$field | length")
  if [ "$actual" -eq "$expected" ]; then
    echo "✅ $field 长度 = $expected"
  else
    echo "❌ $field 长度: expected $expected, got $actual"
    return 1
  fi
}

# 断言响应包含特定字符串
assert_body_contains() {
  local url="$1" expected="$2"
  local body=$(curl -s "$url")
  if echo "$body" | grep -q "$expected"; then
    echo "✅ 响应包含 '$expected'"
  else
    echo "❌ 响应不包含 '$expected'"
    return 1
  fi
}

# 使用示例
assert_field "https://api.example.com/users/1" ".name" "张三"
assert_field "https://api.example.com/users/1" ".role" "admin"
assert_array_length "https://api.example.com/users" "." 50
assert_body_contains "https://example.com" "Welcome"

13.5 CI/CD 集成

GitHub Actions 集成

# .github/workflows/api-test.yml
name: API Tests
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  api-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Wait for deployment
        run: |
          for i in $(seq 1 30); do
            if curl -sf https://staging.example.com/health > /dev/null; then
              echo "服务就绪"
              exit 0
            fi
            echo "等待服务启动... ($i/30)"
            sleep 10
          done
          echo "服务启动超时"
          exit 1
      
      - name: Run API tests
        run: |
          chmod +x tests/api_test.sh
          ./tests/api_test.sh https://staging.example.com
      
      - name: Health check
        run: |
          HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
            https://staging.example.com/health)
          if [ "$HTTP_CODE" != "200" ]; then
            echo "健康检查失败: HTTP $HTTP_CODE"
            exit 1
          fi

GitLab CI 集成

# .gitlab-ci.yml
api-test:
  stage: test
  image: curlimages/curl:latest
  script:
    - |
      for i in $(seq 1 30); do
        if curl -sf http://app:8080/health > /dev/null; then
          echo "服务就绪"
          break
        fi
        sleep 5
      done
    - |
      HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://app:8080/api/data)
      if [ "$HTTP_CODE" != "200" ]; then
        echo "API 测试失败: HTTP $HTTP_CODE"
        exit 1
      fi
    - curl -s http://app:8080/api/users | jq 'length > 0'

13.6 Mock 服务测试

# 使用 httpbin 模拟各种 HTTP 场景
# https://httpbin.org 或自建 httpbin

# 模拟 429 限流
curl -s -o /dev/null -w "%{http_code}" https://httpbin.org/status/429
# 返回 429

# 模拟慢响应
curl -s --max-time 10 https://httpbin.org/delay/5 | jq '.url'

# 模拟随机失败
for i in $(seq 1 10); do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
    "https://httpbin.org/status/200,500")
  echo "请求 $i: HTTP $STATUS"
done

# 使用 echo 模拟 API 响应(无需网络)
mock_api() {
  echo '{"id": 1, "name": "mock user", "status": "active"}'
}
MOCK_RESULT=$(mock_api)
echo "$MOCK_RESULT" | jq '.name'

注意事项

  1. jq 依赖:许多 API 测试脚本依赖 jq,确保 CI 环境安装了 jq
  2. 超时设置:API 测试必须设置超时,避免永远等待
  3. 幂等性:测试应设计为可重复运行,不产生副作用
  4. 环境隔离:使用专用测试环境,避免影响生产数据
  5. 敏感信息:CI/CD 中使用 Secrets/Variables 存储 Token
# 使用环境变量管理敏感信息
API_TOKEN="${API_TOKEN:?错误:未设置 API_TOKEN 环境变量}"
BASE_URL="${API_BASE_URL:-http://localhost:8080}"

curl -sS -H "Authorization: Bearer $API_TOKEN" "$BASE_URL/users"

扩展阅读


📖 下一章第 14 章:Docker 中的 curl — 在容器环境中使用 curl,包括健康检查和容器间通信。