# Python+Requests实战:5分钟搞定HTTP接口自动化测试(附完整代码)
如果你刚接触接口自动化测试,可能会觉得这是个需要复杂框架和大量配置的“大工程”。但我想告诉你,用Python的Requests库,你完全可以在5分钟内跑通第一个自动化测试脚本。这不是夸张,而是我亲身实践过无数次的高效路径。很多团队在初期过度追求框架的“完备性”,反而忽略了快速验证和迭代的价值。今天,我们就抛开那些厚重的理论,直接从一行代码开始,看看如何用最轻量的方式,让接口测试自动化起来。
这篇文章面向的是那些希望快速上手、用代码解决实际测试问题的开发者或测试工程师。你可能已经用过Postman手动点一点,但面对几十上百个接口的回归测试时,手动操作不仅效率低下,而且容易出错。我们将聚焦于HTTP API接口,使用Python这个对新手极其友好的语言,以及Requests这个“人类友好”的HTTP库,一步步构建出可运行、可复用的测试代码。你会发现,自动化测试的门槛,其实比你想象的要低得多。
## 1. 环境准备与第一个请求
万事开头难,但我们的开头会非常简单。你不需要安装庞大的IDE,甚至不需要复杂的项目结构。一个能运行Python的环境,加上一个文本编辑器,就足够了。
首先,确保你的机器上安装了Python。打开终端或命令行,输入 `python --version` 或 `python3 --version`。如果能看到类似 `Python 3.8.10` 的版本信息,说明环境已经就绪。我强烈建议使用Python 3.6或更高版本,它能提供更好的语法支持和库兼容性。
接下来,安装我们唯一的核心依赖:Requests库。它几乎成了Python领域处理HTTP请求的事实标准,其API设计之优雅,让发送请求变得像说话一样自然。
```bash
pip install requests
```
如果网络环境导致安装缓慢,可以使用国内的镜像源加速,例如:
```bash
pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple
```
安装完成后,让我们立刻来感受一下它的威力。假设我们要测试一个获取用户信息的公开接口 `https://jsonplaceholder.typicode.com/users/1`。创建一个名为 `first_test.py` 的文件,输入以下代码:
```python
import requests
# 发送一个最简单的GET请求
response = requests.get('https://jsonplaceholder.typicode.com/users/1')
# 打印响应的状态码
print(f"状态码: {response.status_code}")
# 打印响应的JSON内容
print("响应体:")
print(response.json())
```
保存文件,然后在命令行运行 `python first_test.py`。几秒钟内,你应该能看到类似下面的输出:
```
状态码: 200
响应体:
{'id': 1, 'name': 'Leanne Graham', 'username': 'Bret', 'email': 'Sincere@april.biz', ...}
```
看,你已经成功完成了一次自动化接口调用!整个过程不到10行代码。`response.status_code` 告诉你请求是否成功(200表示成功),`response.json()` 则自动将返回的JSON字符串解析成了Python字典,方便你后续检查。这就是Requests库的魅力——它帮你处理了底层的网络连接、编码解码等繁琐细节,让你能专注于测试逻辑本身。
> **提示**:在实际项目中,你测试的可能是公司内部的开发、测试或预发环境地址。请务必确保你有权限访问这些地址,并且遵守相关的安全规范。切勿对生产环境进行未经授权的测试。
## 2. 构造多样化的HTTP请求
现实世界的接口远不止简单的GET请求。登录、提交订单、上传文件……这些操作对应着POST、PUT、DELETE等多种HTTP方法,并且需要携带各种参数。Requests库为每种常见的HTTP方法都提供了直观的函数。
### 2.1 发送带参数的GET请求
很多查询接口需要通过URL参数(Query Parameters)来传递条件。比如,搜索用户时,我们可能需要传递 `page` 和 `limit` 参数。
```python
import requests
# 方法一:手动拼接URL(不推荐,容易出错)
url_with_params = 'https://api.example.com/users?page=2&limit=10'
response = requests.get(url_with_params)
# 方法二:使用params字典(推荐,更清晰且自动编码)
params = {'page': 2, 'limit': 10}
response = requests.get('https://api.example.com/users', params=params)
print(f"最终请求的URL: {response.url}") # 可以看到参数已被正确编码和附加
print(response.json())
```
使用 `params` 字典的好处是,Requests会自动处理特殊字符的编码(比如空格会被转换成`%20`),避免因手动拼接导致的错误。
### 2.2 发送POST请求与请求体
创建资源,比如新增一个用户,通常使用POST方法,并将数据放在请求体(Body)中。最常见的数据格式是JSON。
```python
import requests
import json
# 要创建的新用户数据
new_user = {
"name": "John Doe",
"email": "john.doe@example.com",
"status": "active"
}
# 发送POST请求,json参数会自动将字典序列化为JSON,并设置正确的Content-Type头
response = requests.post('https://api.example.com/users', json=new_user)
print(f"状态码: {response.status_code}")
print(f"响应: {response.json()}")
```
有时候,接口可能要求使用 `application/x-www-form-urlencoded` 格式(类似于HTML表单提交)。这时可以使用 `data` 参数:
```python
login_data = {
'username': 'testuser',
'password': 'testpass123'
}
response = requests.post('https://api.example.com/login', data=login_data)
```
### 2.3 处理请求头与认证
许多接口需要认证信息或特定的请求头。例如,携带Token进行身份验证,或者指定接收的数据格式。
```python
import requests
# 定义请求头
headers = {
'Authorization': 'Bearer your_access_token_here', # JWT Token认证
'Content-Type': 'application/json',
'User-Agent': 'MyAutomationTest/1.0'
}
# 在请求中传入headers
response = requests.get('https://api.example.com/protected/resource', headers=headers)
# 另一种常见的认证方式是HTTP Basic Auth
from requests.auth import HTTPBasicAuth
response = requests.get('https://api.example.com/secure',
auth=HTTPBasicAuth('username', 'password'))
```
为了让你对不同请求方式有一个清晰的对比,我整理了下面这个表格:
| 请求方法 | 常用场景 | Requests函数 | 关键参数 | 示例 |
| :--- | :--- | :--- | :--- | :--- |
| **GET** | 获取资源 | `requests.get()` | `params`, `headers` | `get('/users', params={'id':1})` |
| **POST** | 创建资源 | `requests.post()` | `json`, `data`, `headers` | `post('/users', json={'name':'John'})` |
| **PUT** | 更新整个资源 | `requests.put()` | `json`, `data`, `headers` | `put('/users/1', json={'name':'Jane'})` |
| **PATCH** | 部分更新资源 | `requests.patch()` | `json`, `data`, `headers` | `patch('/users/1', json={'email':'new@mail.com'})` |
| **DELETE** | 删除资源 | `requests.delete()` | `headers` | `delete('/users/1')` |
掌握了这些基本请求的构造方法,你已经能够覆盖绝大多数API的调用场景。接下来,我们需要让脚本不仅能发送请求,还能智能地判断请求结果是否正确。
## 3. 断言与响应验证
发送请求只是第一步,自动化测试的核心在于“断言”——即自动检查响应是否符合预期。一个没有断言的测试脚本,就像没有刹车的汽车,跑得再快也不知道是否到达了目的地。
### 3.1 基础断言:状态码与响应体
最直接的断言就是检查HTTP状态码。2xx通常代表成功,4xx代表客户端错误,5xx代表服务器错误。
```python
import requests
response = requests.get('https://jsonplaceholder.typicode.com/users/1')
# 断言状态码为200
assert response.status_code == 200, f"预期状态码200,实际得到{response.status_code}"
# 断言响应中包含特定字段和值
response_data = response.json()
assert response_data['id'] == 1, f"用户ID预期为1,实际为{response_data['id']}"
assert 'email' in response_data, "响应中未找到email字段"
assert '@' in response_data['email'], f"邮箱格式不正确: {response_data['email']}"
print("所有基础断言通过!")
```
如果断言失败,Python会抛出 `AssertionError` 并终止脚本,同时显示我们自定义的错误信息。这能帮助我们快速定位问题。
### 3.2 使用unittest框架进行结构化断言
虽然直接用 `assert` 语句简单直接,但在构建稍复杂的测试套件时,使用Python内置的 `unittest` 框架会更有利于测试的组织和管理。它提供了更丰富的断言方法,并能生成更清晰的测试报告。
```python
import unittest
import requests
class TestUserAPI(unittest.TestCase):
BASE_URL = 'https://jsonplaceholder.typicode.com'
def test_get_existing_user(self):
"""测试获取已存在的用户"""
response = requests.get(f'{self.BASE_URL}/users/1')
# 使用unittest的断言方法
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data['id'], 1)
self.assertIsInstance(data['name'], str)
self.assertGreater(len(data['name']), 0) # 断言名字长度大于0
def test_get_nonexistent_user(self):
"""测试获取不存在的用户(应返回404)"""
response = requests.get(f'{self.BASE_URL}/users/999')
self.assertEqual(response.status_code, 404)
def test_create_user(self):
"""测试创建新用户(此示例API为模拟,实际可能不持久化)"""
new_user = {"name": "Test User", "username": "testuser"}
response = requests.post(f'{self.BASE_URL}/users', json=new_user)
self.assertEqual(response.status_code, 201) # 201 Created
response_data = response.json()
# 检查返回的数据包含我们发送的数据
self.assertEqual(response_data['name'], new_user['name'])
self.assertEqual(response_data['username'], new_user['username'])
# 检查服务器生成了id
self.assertIn('id', response_data)
if __name__ == '__main__':
unittest.main()
```
将上述代码保存为 `test_with_unittest.py`,直接运行这个文件。`unittest` 会自动发现以 `test_` 开头的方法并执行。你会看到类似如下的输出,清晰地展示了每个测试用例的执行结果:
```
...
----------------------------------------------------------------------
Ran 3 tests in 1.234s
OK
```
如果某个测试失败,它会明确指出是哪个用例的哪条断言出了问题,非常利于调试。
### 3.3 验证响应时间与性能基线
除了功能正确性,接口的性能也是测试需要关注的点。一个接口如果耗时超过预期,也会影响用户体验。
```python
import requests
import time
def test_response_time():
"""测试接口响应时间是否在可接受范围内"""
start_time = time.time()
response = requests.get('https://jsonplaceholder.typicode.com/users')
end_time = time.time()
elapsed_time = end_time - start_time
print(f"请求耗时: {elapsed_time:.3f} 秒")
# 断言响应时间小于2秒
assert elapsed_time < 2.0, f"接口响应过慢,耗时 {elapsed_time:.3f} 秒"
assert response.status_code == 200
print("性能测试通过。")
# 可以多次测试取平均值,结果更稳定
total_time = 0
num_requests = 5
for i in range(num_requests):
start = time.time()
requests.get('https://jsonplaceholder.typicode.com/users')
total_time += (time.time() - start)
time.sleep(0.5) # 短暂间隔,避免对服务器造成压力
avg_time = total_time / num_requests
print(f"平均响应时间: {avg_time:.3f} 秒")
```
将功能断言和性能检查结合起来,你的测试脚本就开始有了“灵魂”。它不再是被动地接收数据,而是主动地验证系统的行为是否符合契约和预期。
## 4. 构建可复用的测试脚手架
当测试用例越来越多时,你会发现大量重复代码:相同的基地址、相同的请求头设置、相同的登录逻辑。这时,我们需要将这些公共部分抽取出来,构建一个轻量级的测试脚手架,让每个测试用例只关注其独特的测试逻辑。
### 4.1 创建基础请求会话
`requests.Session()` 对象可以跨请求保持某些参数,如cookies、headers,甚至适配器配置。这非常适合用来模拟一个“用户会话”。
```python
import requests
class APIClient:
def __init__(self, base_url):
self.base_url = base_url
self.session = requests.Session()
# 设置公共请求头
self.session.headers.update({
'User-Agent': 'APITestClient/1.0',
'Accept': 'application/json',
})
def set_auth_token(self, token):
"""设置认证Token"""
self.session.headers['Authorization'] = f'Bearer {token}'
def get(self, endpoint, **kwargs):
"""发送GET请求"""
url = f"{self.base_url}{endpoint}"
return self.session.get(url, **kwargs)
def post(self, endpoint, data=None, json=None, **kwargs):
"""发送POST请求"""
url = f"{self.base_url}{endpoint}"
return self.session.post(url, data=data, json=json, **kwargs)
# 类似地,可以封装put, patch, delete等方法
# 使用示例
if __name__ == '__main__':
client = APIClient('https://jsonplaceholder.typicode.com')
# 所有通过client发起的请求都会自动带上公共请求头
resp1 = client.get('/users/1')
print(resp1.json()['name'])
# 模拟登录后设置token
# client.set_auth_token('fake_jwt_token_here')
# resp2 = client.get('/protected/resource')
```
### 4.2 封装通用断言与工具函数
我们可以把常用的断言逻辑也封装起来,让测试用例更简洁。
```python
import json
class Assertions:
@staticmethod
def assert_status_code(response, expected_code):
assert response.status_code == expected_code, \
f"断言失败!预期状态码: {expected_code}, 实际: {response.status_code}, 响应体: {response.text[:200]}"
@staticmethod
def assert_json_has_key(response, key):
data = response.json()
assert key in data, f"响应JSON中未找到键 '{key}'。完整响应: {json.dumps(data, indent=2)[:500]}"
@staticmethod
def assert_json_value_equal(response, key, expected_value):
data = response.json()
actual_value = data.get(key)
assert actual_value == expected_value, \
f"键 '{key}' 的值不匹配。预期: {expected_value}, 实际: {actual_value}"
# 在测试用例中使用
client = APIClient('https://jsonplaceholder.typicode.com')
resp = client.get('/users/1')
Assertions.assert_status_code(resp, 200)
Assertions.assert_json_has_key(resp, 'name')
Assertions.assert_json_value_equal(resp, 'id', 1)
```
### 4.3 组织测试用例与数据驱动
对于同一个接口,我们经常需要用多组不同的输入数据去测试(正常值、边界值、异常值)。`unittest` 框架的 `@parameterized.expand` 装饰器(需要安装 `parameterized` 库)或 `pytest` 的 `@pytest.mark.parametrize` 可以优雅地实现数据驱动测试。
以下是一个使用 `pytest` 的示例(首先需要 `pip install pytest`):
```python
# test_data_driven.py
import pytest
import requests
BASE_URL = 'https://jsonplaceholder.typicode.com'
# 测试数据:用户ID列表
test_user_ids = [
(1, 200, 'Leanne Graham'), # (输入, 预期状态码, 预期姓名)
(2, 200, 'Ervin Howell'),
(10, 200, 'Clementina DuBuque'),
(0, 404, None), # 不存在的ID,预期404
(999, 404, None),
]
@pytest.mark.parametrize("user_id, expected_status, expected_name", test_user_ids)
def test_get_user_by_id(user_id, expected_status, expected_name):
"""数据驱动测试:使用多组数据测试获取用户接口"""
response = requests.get(f'{BASE_URL}/users/{user_id}')
assert response.status_code == expected_status
if expected_status == 200:
data = response.json()
assert data['id'] == user_id
if expected_name:
assert data['name'] == expected_name
else:
# 对于404等错误,可以断言响应体为空或包含错误信息
assert response.json() == {} # 该示例API对于404返回空对象
```
在命令行中运行 `pytest test_data_driven.py -v`,你会看到每个参数组合都被作为一个独立的测试用例执行,结果一目了然。数据驱动极大地减少了编写重复测试代码的工作量。
## 5. 实战:一个完整的迷你测试项目
现在,让我们把前面所有的知识点串联起来,构建一个针对“待办事项(Todo)”API的完整迷你测试项目。这个项目将包含配置管理、测试用例、工具函数,并最终生成一个简单的测试报告。
**项目结构**
```
todo_api_tests/
├── config.py # 配置文件
├── api_client.py # 封装的API客户端
├── test_todos.py # 测试用例
└── run_tests.py # 测试运行入口
```
**1. config.py - 集中管理配置**
```python
# config.py
class Config:
# 基础URL,可以根据环境切换
BASE_URL = 'https://jsonplaceholder.typicode.com'
# 全局超时时间(秒)
REQUEST_TIMEOUT = 10
# 是否启用详细日志
VERBOSE = True
```
**2. api_client.py - 增强的API客户端**
```python
# api_client.py
import requests
from config import Config
import logging
logging.basicConfig(level=logging.INFO if Config.VERBOSE else logging.WARNING)
logger = logging.getLogger(__name__)
class TodoAPIClient:
def __init__(self):
self.base_url = Config.BASE_URL
self.session = requests.Session()
self.session.headers.update({
'Content-Type': 'application/json',
'Accept': 'application/json'
})
self.timeout = Config.REQUEST_TIMEOUT
def _log_request(self, method, url, **kwargs):
if Config.VERBOSE:
logger.info(f"发送请求: {method} {url}")
if 'json' in kwargs:
logger.debug(f"请求体: {kwargs['json']}")
def _log_response(self, response):
if Config.VERBOSE:
logger.info(f"收到响应: {response.status_code}")
logger.debug(f"响应体: {response.text[:500]}") # 只打印前500字符
def _request(self, method, endpoint, **kwargs):
url = f"{self.base_url}{endpoint}"
self._log_request(method, url, **kwargs)
try:
response = self.session.request(method, url, timeout=self.timeout, **kwargs)
self._log_response(response)
return response
except requests.exceptions.Timeout:
logger.error(f"请求超时: {url}")
raise
except requests.exceptions.RequestException as e:
logger.error(f"请求异常: {e}")
raise
# 封装具体的业务接口方法
def get_all_todos(self):
return self._request('GET', '/todos')
def get_todo_by_id(self, todo_id):
return self._request('GET', f'/todos/{todo_id}')
def create_todo(self, title, completed=False, userId=1):
payload = {
'title': title,
'completed': completed,
'userId': userId
}
return self._request('POST', '/todos', json=payload)
def update_todo(self, todo_id, **kwargs):
"""部分更新Todo,传入需要更新的字段"""
return self._request('PATCH', f'/todos/{todo_id}', json=kwargs)
def delete_todo(self, todo_id):
return self._request('DELETE', f'/todos/{todo_id}')
```
**3. test_todos.py - 核心测试用例**
```python
# test_todos.py
import pytest
from api_client import TodoAPIClient
client = TodoAPIClient()
class TestTodoAPI:
"""Todo API测试套件"""
def test_get_all_todos(self):
"""测试获取所有待办事项"""
response = client.get_all_todos()
assert response.status_code == 200
todos = response.json()
# 断言返回的是列表,且至少有一个元素
assert isinstance(todos, list)
assert len(todos) > 0
# 检查列表中的每个元素都有必要的字段
for todo in todos[:5]: # 抽样检查前5个
assert 'id' in todo
assert 'title' in todo
assert 'completed' in todo
assert isinstance(todo['completed'], bool)
def test_get_specific_todo(self):
"""测试获取指定ID的待办事项"""
response = client.get_todo_by_id(1)
assert response.status_code == 200
todo = response.json()
assert todo['id'] == 1
assert 'title' in todo and len(todo['title']) > 0
@pytest.mark.parametrize("todo_id", [0, 999, -1])
def test_get_nonexistent_todo(self, todo_id):
"""测试获取不存在的待办事项(应返回404)"""
response = client.get_todo_by_id(todo_id)
# 该模拟API对于不存在的资源返回空对象,而不是404
# 这里我们根据实际API行为调整断言
assert response.status_code == 200 # 注意:此示例API总是返回200
# 更常见的断言是:assert response.status_code == 404
def test_create_and_delete_todo(self):
"""测试创建并删除一个待办事项(集成测试)"""
# 1. 创建
new_title = "学习Python接口自动化测试"
create_resp = client.create_todo(title=new_title, completed=False)
assert create_resp.status_code == 201
created_todo = create_resp.json()
assert created_todo['title'] == new_title
assert created_todo['completed'] is False
assert 'id' in created_todo
new_id = created_todo['id']
# 2. 验证创建成功(可选)
get_resp = client.get_todo_by_id(new_id)
assert get_resp.status_code == 200
assert get_resp.json()['title'] == new_title
# 3. 删除(注意:此示例API是模拟的,实际不会真正删除)
delete_resp = client.delete_todo(new_id)
assert delete_resp.status_code == 200 # 或204 No Content
# 4. 验证删除后获取不到(此API模拟删除后仍能获取,故注释)
# get_after_delete = client.get_todo_by_id(new_id)
# assert get_after_delete.status_code == 404
def test_update_todo(self):
"""测试更新待办事项"""
# 先获取一个现有的todo进行更新
todo_id = 1
original_resp = client.get_todo_by_id(todo_id)
original_todo = original_resp.json()
# 更新标题和完成状态
updated_title = f"已更新: {original_todo['title']}"
update_resp = client.update_todo(todo_id, title=updated_title, completed=True)
assert update_resp.status_code == 200
updated_todo = update_resp.json()
assert updated_todo['title'] == updated_title
assert updated_todo['completed'] is True
# 确保id没变
assert updated_todo['id'] == todo_id
```
**4. run_tests.py - 运行并生成报告**
```python
# run_tests.py
import pytest
import sys
from datetime import datetime
def run_tests():
"""运行测试并生成简单报告"""
print(f"开始执行Todo API测试套件 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("="*60)
# 使用pytest运行测试,并指定详细输出
# -v: 详细模式
# --tb=short: 简化错误回溯信息
# -s: 允许打印(如我们的logger信息)
exit_code = pytest.main(['test_todos.py', '-v', '--tb=short', '-s'])
print("="*60)
if exit_code == 0:
print("✅ 所有测试通过!")
else:
print(f"❌ 测试失败,退出码: {exit_code}")
return exit_code
if __name__ == '__main__':
sys.exit(run_tests())
```
现在,在项目根目录下执行 `python run_tests.py`,你将看到一个结构清晰、反馈详细的测试执行过程。这个迷你项目虽然简单,但已经具备了真实项目测试套件的雏形:配置与代码分离、业务逻辑封装、清晰的测试用例、以及可执行的测试入口。
走到这里,你已经掌握了用Python+Requests构建接口自动化测试的核心技能。从发送第一个GET请求,到构建一个结构清晰、可维护的测试项目,整个过程强调的是“快速上手”和“解决实际问题”。我见过太多团队在自动化测试的起步阶段陷入工具选型或框架设计的争论,而忘了最根本的目标——尽快让测试跑起来,并持续提供价值。用这不到200行的代码,你已经可以开始为你的项目编写有价值的自动化测试了。接下来要做的,就是在实际项目中不断实践、迭代和优化这套模式。