Postman 自动化测试完全指南3
·
第七章:认证与安全机制
7.1 认证类型概览
7.1.1 Postman 支持的认证类型
7.1.2 认证配置位置
认证配置层级:
1. Collection 级别:应用于集合内所有请求
2. Folder 级别:应用于文件夹内所有请求
3. Request 级别:仅应用于单个请求
优先级:Request > Folder > Collection
7.2 API Key 认证
7.2.1 API Key 认证原理
7.2.2 API Key 配置方式
方式一:Header 传递
GET /api/v1/users HTTP/1.1
Host: api.example.com
X-API-Key: your-api-key-here
Postman 配置:
Authorization Tab:
Type: API Key
Key: X-API-Key
Value: {{apiKey}}
Add to: Header
方式二:Query 参数传递
GET /api/v1/users?api_key=your-api-key-here HTTP/1.1
Host: api.example.com
Postman 配置:
Authorization Tab:
Type: API Key
Key: api_key
Value: {{apiKey}}
Add to: Query Parameters
脚本动态设置:
// Pre-request Script 动态设置 API Key
const apiKey = pm.environment.get('apiKey');
const timestamp = Date.now();
// 生成带时间戳的签名 Key
const signedKey = CryptoJS.MD5(apiKey + timestamp).toString();
pm.request.headers.add({
key: 'X-API-Key',
value: signedKey
});
pm.request.headers.add({
key: 'X-Timestamp',
value: timestamp.toString()
});
7.3 Basic Auth 认证
7.3.1 Basic Auth 原理
Basic Auth 使用 Base64 编码的用户名和密码进行认证。
7.3.2 Basic Auth 配置
Postman 配置:
Authorization Tab:
Type: Basic Auth
Username: {{username}}
Password: {{password}}
手动设置请求头:
// Pre-request Script 设置 Basic Auth
const username = pm.environment.get('username');
const password = pm.environment.get('password');
// 生成 Basic Auth 头
const credentials = btoa(`${username}:${password}`);
pm.request.headers.upsert({
key: 'Authorization',
value: `Basic ${credentials}`
});
验证 Basic Auth 响应:
// Tests Script 验证 Basic Auth
pm.test('Basic Auth 认证应成功', function() {
pm.response.to.have.status(200);
});
pm.test('应返回用户信息', function() {
const response = pm.response.json();
pm.expect(response.data).to.have.property('username');
});
// 处理认证失败
if (pm.response.code === 401) {
const authHeader = pm.response.headers.get('WWW-Authenticate');
console.log('认证失败,服务器要求:', authHeader);
}
7.4 Bearer Token 认证
7.4.1 Bearer Token 原理
7.4.2 Bearer Token 配置
Postman 配置:
Authorization Tab:
Type: Bearer Token
Token: {{token}}
自动获取并保存 Token:
// 登录请求 Tests Script
const response = pm.response.json();
if (response.code === 200 && response.data.token) {
// 保存 Token
pm.environment.set('token', response.data.token);
// 保存过期时间
if (response.data.expiresIn) {
const expireTime = Date.now() + response.data.expiresIn * 1000;
pm.environment.set('tokenExpireTime', expireTime);
}
// 保存刷新 Token
if (response.data.refreshToken) {
pm.environment.set('refreshToken', response.data.refreshToken);
}
console.log('Token 已保存');
}
// 验证 Token 格式
pm.test('Token 格式应正确', function() {
pm.expect(response.data.token).to.be.a('string');
pm.expect(response.data.token.length).to.be.above(0);
});
Token 自动刷新机制:
// Pre-request Script - 检查并刷新 Token
const tokenExpireTime = parseInt(pm.environment.get('tokenExpireTime') || '0');
const refreshThreshold = 5 * 60 * 1000; // 5分钟前刷新
if (Date.now() > tokenExpireTime - refreshThreshold) {
console.log('Token 即将过期,尝试刷新...');
// 发送刷新请求
pm.sendRequest({
url: pm.environment.get('baseUrl') + '/api/v1/auth/refresh',
method: 'POST',
header: {
'Content-Type': 'application/json'
},
body: {
mode: 'raw',
raw: JSON.stringify({
refreshToken: pm.environment.get('refreshToken')
})
}
}, (error, response) => {
if (!error && response.code === 200) {
const data = response.json();
pm.environment.set('token', data.data.token);
pm.environment.set('tokenExpireTime', Date.now() + data.data.expiresIn * 1000);
console.log('Token 刷新成功');
} else {
console.error('Token 刷新失败,请重新登录');
}
});
}
// 设置当前请求的 Token
const token = pm.environment.get('token');
if (token) {
pm.request.headers.upsert({
key: 'Authorization',
value: `Bearer ${token}`
});
}
7.5 OAuth 2.0 认证
7.5.1 OAuth 2.0 授权流程
7.5.2 OAuth 2.0 授权类型
| 授权类型 | 适用场景 | 特点 |
|---|---|---|
| Authorization Code | Web 应用 | 最安全,需要授权码 |
| Implicit | 单页应用 | 直接返回 Token |
| Resource Owner Password | 受信任应用 | 直接使用用户名密码 |
| Client Credentials | 服务间通信 | 无用户参与 |
7.5.3 Authorization Code 流程配置
Postman 配置:
Authorization Tab:
Type: OAuth 2.0
Grant Type: Authorization Code
Callback URL: https://oauth.pstmn.io/v1/callback
Auth URL: https://auth.example.com/oauth/authorize
Access Token URL: https://auth.example.com/oauth/token
Client ID: {{clientId}}
Client Secret: {{clientSecret}}
Scope: read write
State: random_state_string
获取授权码:
// Pre-request Script - 构建授权 URL
const clientId = pm.environment.get('clientId');
const redirectUri = 'https://oauth.pstmn.io/v1/callback';
const scope = 'read write';
const state = Math.random().toString(36).substring(7);
// 保存 state 用于验证
pm.environment.set('oauthState', state);
const authUrl = `https://auth.example.com/oauth/authorize?` +
`response_type=code&` +
`client_id=${clientId}&` +
`redirect_uri=${encodeURIComponent(redirectUri)}&` +
`scope=${encodeURIComponent(scope)}&` +
`state=${state}`;
console.log('授权 URL:', authUrl);
使用授权码获取 Token:
// Tests Script - 处理授权码回调
const queryParams = pm.request.url.query.toObject();
const code = queryParams.code;
const state = queryParams.state;
// 验证 state
if (state !== pm.environment.get('oauthState')) {
console.error('State 不匹配,可能存在 CSRF 攻击');
}
if (code) {
// 使用授权码获取 Token
pm.sendRequest({
url: 'https://auth.example.com/oauth/token',
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: {
mode: 'urlencoded',
urlencoded: [
{ key: 'grant_type', value: 'authorization_code' },
{ key: 'code', value: code },
{ key: 'redirect_uri', value: 'https://oauth.pstmn.io/v1/callback' },
{ key: 'client_id', value: pm.environment.get('clientId') },
{ key: 'client_secret', value: pm.environment.get('clientSecret') }
]
}
}, (error, response) => {
if (!error) {
const data = response.json();
pm.environment.set('accessToken', data.access_token);
pm.environment.set('refreshToken', data.refresh_token);
pm.environment.set('tokenExpireTime', Date.now() + data.expires_in * 1000);
console.log('OAuth Token 获取成功');
}
});
}
7.5.4 Client Credentials 流程
// Client Credentials 授权
// Pre-request Script
const clientId = pm.environment.get('clientId');
const clientSecret = pm.environment.get('clientSecret');
// 检查是否需要获取新 Token
const tokenExpireTime = parseInt(pm.environment.get('clientTokenExpireTime') || '0');
if (Date.now() > tokenExpireTime - 60000) { // 提前1分钟刷新
// 获取 Client Credentials Token
pm.sendRequest({
url: pm.environment.get('baseUrl') + '/oauth/token',
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + btoa(`${clientId}:${clientSecret}`)
},
body: {
mode: 'urlencoded',
urlencoded: [
{ key: 'grant_type', value: 'client_credentials' },
{ key: 'scope', value: 'api:read api:write' }
]
}
}, (error, response) => {
if (!error && response.code === 200) {
const data = response.json();
pm.environment.set('clientToken', data.access_token);
pm.environment.set('clientTokenExpireTime', Date.now() + data.expires_in * 1000);
console.log('Client Token 获取成功');
}
});
}
// 设置请求 Token
const clientToken = pm.environment.get('clientToken');
if (clientToken) {
pm.request.headers.upsert({
key: 'Authorization',
value: `Bearer ${clientToken}`
});
}
7.6 JWT 认证
7.6.1 JWT 结构解析
7.6.2 JWT 解析与验证
// JWT 解析函数
function parseJWT(token) {
try {
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('无效的 JWT 格式');
}
const header = JSON.parse(atob(parts[0]));
const payload = JSON.parse(atob(parts[1]));
const signature = parts[2];
return {
header: header,
payload: payload,
signature: signature
};
} catch (error) {
console.error('JWT 解析失败:', error.message);
return null;
}
}
// 使用示例
const token = pm.environment.get('token');
const jwt = parseJWT(token);
if (jwt) {
console.log('JWT Header:', jwt.header);
console.log('JWT Payload:', jwt.payload);
// 验证过期时间
if (jwt.payload.exp) {
const expireTime = jwt.payload.exp * 1000;
if (Date.now() > expireTime) {
console.warn('JWT 已过期');
} else {
const remaining = Math.floor((expireTime - Date.now()) / 1000 / 60);
console.log(`JWT 剩余有效时间: ${remaining} 分钟`);
}
}
// 提取用户信息
if (jwt.payload.sub) {
pm.environment.set('jwtUserId', jwt.payload.sub);
}
if (jwt.payload.username) {
pm.environment.set('jwtUsername', jwt.payload.username);
}
}
7.6.3 JWT 签名验证
// JWT 签名验证
function verifyJWTSignature(token, secret) {
const parts = token.split('.');
if (parts.length !== 3) {
return false;
}
const header = parts[0];
const payload = parts[1];
const signature = parts[2];
// 重新计算签名
const expectedSignature = CryptoJS.HmacSHA256(
`${header}.${payload}`,
secret
).toString(CryptoJS.enc.Base64Url);
return signature === expectedSignature;
}
// 使用示例
const token = pm.environment.get('token');
const secret = pm.environment.get('jwtSecret');
pm.test('JWT 签名应有效', function() {
// 注意:在生产环境中,签名验证应在服务器端进行
// 这里仅用于测试目的
const isValid = verifyJWTSignature(token, secret);
pm.expect(isValid).to.be.true;
});
7.7 自定义认证方案
7.7.1 自定义签名认证
// 自定义签名认证方案
function generateSignature(params, secret) {
// 1. 按键名排序
const sortedKeys = Object.keys(params).sort();
// 2. 拼接参数字符串
const paramString = sortedKeys
.filter(key => params[key] !== undefined && params[key] !== '')
.map(key => `${key}=${params[key]}`)
.join('&');
// 3. 添加密钥
const signString = paramString + secret;
// 4. 计算签名
return CryptoJS.MD5(signString).toString().toUpperCase();
}
// Pre-request Script
const appId = pm.environment.get('appId');
const appSecret = pm.environment.get('appSecret');
const timestamp = Date.now();
const nonce = Math.random().toString(36).substring(2, 15);
// 构建签名参数
const signParams = {
appId: appId,
timestamp: timestamp,
nonce: nonce,
userId: pm.environment.get('userId')
};
// 生成签名
const signature = generateSignature(signParams, appSecret);
// 添加认证头
pm.request.headers.add({ key: 'X-App-ID', value: appId });
pm.request.headers.add({ key: 'X-Timestamp', value: timestamp.toString() });
pm.request.headers.add({ key: 'X-Nonce', value: nonce });
pm.request.headers.add({ key: 'X-Signature', value: signature });
console.log('签名参数:', signParams);
console.log('生成的签名:', signature);
7.7.2 双向证书认证
// 双向证书认证配置
// 在 Pre-request Script 中配置证书信息
// 注意:实际证书配置需要在 Postman 设置中进行
const certConfig = {
certPath: '/path/to/client.crt',
keyPath: '/path/to/client.key',
passphrase: pm.environment.get('certPassphrase')
};
// 验证服务器证书
pm.test('服务器证书应有效', function() {
const response = pm.response;
// 检查是否使用 HTTPS
pm.expect(pm.request.url.protocol).to.equal('https');
});
7.8 安全最佳实践
7.8.1 敏感信息保护
// 敏感信息保护最佳实践
// 1. 使用 Secret 类型变量
// 在环境变量中将敏感信息设置为 Secret 类型
// 2. 不在日志中输出敏感信息
const token = pm.environment.get('token');
// 错误做法
// console.log('Token:', token);
// 正确做法
console.log('Token 已获取,长度:', token ? token.length : 0);
// 3. 请求完成后清除临时敏感数据
pm.environment.unset('tempPassword');
pm.variables.unset('tempKey');
// 4. 使用环境变量而非硬编码
// 错误做法
// const apiKey = 'sk-xxxxx';
// 正确做法
const apiKey = pm.environment.get('apiKey');
// 5. 定期轮换密钥
const keyLastRotated = parseInt(pm.environment.get('keyLastRotated') || '0');
const rotationPeriod = 90 * 24 * 60 * 60 * 1000; // 90天
if (Date.now() - keyLastRotated > rotationPeriod) {
console.warn('API Key 已超过 90 天未轮换,建议更新');
}
7.8.2 安全测试检查清单
// 安全测试检查清单
const response = pm.response.json();
// 1. 验证 HTTPS
pm.test('应使用 HTTPS 协议', function() {
pm.expect(pm.request.url.protocol).to.equal('https');
});
// 2. 验证安全响应头
pm.test('应包含安全响应头', function() {
const headers = {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': /.*/
};
Object.entries(headers).forEach(([header, expected]) => {
const value = pm.response.headers.get(header);
if (expected instanceof RegExp) {
pm.expect(value).to.match(expected);
} else {
pm.expect(value).to.equal(expected);
}
});
});
// 3. 验证不暴露敏感信息
pm.test('响应不应包含敏感字段', function() {
const sensitiveFields = ['password', 'secret', 'apiKey', 'privateKey'];
const responseStr = JSON.stringify(response);
sensitiveFields.forEach(field => {
pm.expect(responseStr.toLowerCase()).to.not.include(field.toLowerCase());
});
});
// 4. 验证错误信息不泄露系统信息
pm.test('错误信息不应泄露系统信息', function() {
if (response.message) {
const systemPatterns = [
/stack trace/i,
/exception/i,
/sql/i,
/path/i,
/file not found/i
];
systemPatterns.forEach(pattern => {
pm.expect(response.message).to.not.match(pattern);
});
}
});
// 5. 验证认证失败不暴露详细信息
if (pm.response.code === 401) {
pm.test('认证失败应返回通用错误信息', function() {
pm.expect(response.message).to.not.include('password');
pm.expect(response.message).to.not.include('user not found');
});
}
7.8.3 防重放攻击
// 防重放攻击机制
// Pre-request Script
const timestamp = Date.now();
const nonce = 'nonce_' + Math.random().toString(36).substring(2, 15);
// 添加时间戳和随机数
pm.request.headers.add({ key: 'X-Timestamp', value: timestamp.toString() });
pm.request.headers.add({ key: 'X-Nonce', value: nonce });
// 记录已使用的 nonce(用于测试验证)
const usedNonces = JSON.parse(pm.environment.get('usedNonces') || '[]');
usedNonces.push({ nonce: nonce, timestamp: timestamp });
// 只保留最近 1000 个
if (usedNonces.length > 1000) {
usedNonces.shift();
}
pm.environment.set('usedNonces', JSON.stringify(usedNonces));
// Tests Script - 验证服务器拒绝重放
pm.test('服务器应拒绝过期的请求', function() {
// 如果请求时间戳过期,应返回 401 或 403
const requestTime = parseInt(pm.request.headers.get('X-Timestamp'));
const maxAge = 5 * 60 * 1000; // 5分钟
if (Date.now() - requestTime > maxAge) {
pm.expect([401, 403]).to.include(pm.response.code);
}
});
第八章:集合管理与数据驱动测试
8.1 Collection 结构设计
8.1.1 Collection 组织原则
8.1.2 Collection 变量与脚本
Collection 级别变量:
{
"variable": [
{ "key": "baseUrl", "value": "https://api.example.com" },
{ "key": "apiVersion", "value": "v1" },
{ "key": "timeout", "value": "30000" },
{ "key": "retryCount", "value": "3" }
]
}
Collection 级别 Pre-request Script:
// Collection Pre-request Script
// 应用于集合内所有请求
// 设置公共请求头
pm.request.headers.add({
key: 'X-API-Version',
value: pm.collectionVariables.get('apiVersion')
});
pm.request.headers.add({
key: 'X-Client-ID',
value: 'Postman-AutoTest-v1.0'
});
// 自动添加认证
const token = pm.environment.get('token');
if (token) {
pm.request.headers.upsert({
key: 'Authorization',
value: `Bearer ${token}`
});
}
// 请求追踪
const traceId = 'trace_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
pm.request.headers.add({ key: 'X-Trace-ID', value: traceId });
pm.variables.set('currentTraceId', traceId);
Collection 级别 Tests Script:
// Collection Tests Script
// 应用于集合内所有请求
// 记录请求信息
console.log('请求完成:', {
url: pm.request.url.toString(),
method: pm.request.method,
status: pm.response.code,
time: pm.response.responseTime + 'ms',
traceId: pm.variables.get('currentTraceId')
});
// 公共断言
pm.test('响应时间应小于 5 秒', function() {
pm.expect(pm.response.responseTime).to.be.below(5000);
});
// 错误处理
if (pm.response.code >= 400) {
console.error('请求失败:', {
status: pm.response.code,
body: pm.response.text()
});
}
8.2 Collection Runner 使用
8.2.1 Collection Runner 界面
8.2.2 运行参数配置
| 参数 | 说明 | 推荐值 |
|---|---|---|
| Iterations | 迭代次数 | 根据数据文件行数 |
| Delay | 请求间隔 | 100-500ms |
| Data File | 数据文件 | CSV/JSON |
| Persist variables | 保存变量 | 按需选择 |
| Stop on error | 遇错停止 | 测试时开启 |
8.2.3 运行结果分析
// 运行结果统计脚本
// 在 Collection Tests 中添加
const results = {
passed: 0,
failed: 0,
skipped: 0,
total: pm.info.iterationCount
};
// 获取当前测试结果
const testResults = pm.test.results();
results.passed = testResults.passed;
results.failed = testResults.failed;
// 计算成功率
const successRate = (results.passed / (results.passed + results.failed) * 100).toFixed(2);
console.log(`成功率: ${successRate}%`);
// 保存结果
pm.environment.set('testResults', JSON.stringify(results));
8.3 数据驱动测试
8.3.1 数据驱动测试原理
8.3.2 CSV 数据文件
数据文件格式:
username,password,email,expectedStatus
testuser1,Pass123!,test1@example.com,200
testuser2,Pass456!,test2@example.com,200
testuser3,short,test3@example.com,400
testuser4,,test4@example.com,400
使用 CSV 数据:
// Pre-request Script - 使用 CSV 数据
// 获取当前迭代的数据
const username = pm.iterationData.get('username');
const password = pm.iterationData.get('password');
const email = pm.iterationData.get('email');
const expectedStatus = pm.iterationData.get('expectedStatus');
// 设置请求体
const requestBody = {
username: username,
password: password,
email: email
};
pm.request.body.raw = JSON.stringify(requestBody);
// 保存期望状态码供测试使用
pm.variables.set('expectedStatus', parseInt(expectedStatus));
console.log(`测试数据: 用户名=${username}, 期望状态=${expectedStatus}`);
Tests Script - 验证结果:
// Tests Script - 数据驱动验证
const expectedStatus = pm.variables.get('expectedStatus');
const username = pm.iterationData.get('username');
pm.test(`用户 ${username} 注册应返回 ${expectedStatus}`, function() {
pm.response.to.have.status(expectedStatus);
});
// 根据期望状态进行不同验证
if (expectedStatus === 200) {
pm.test('成功注册应返回用户ID', function() {
const response = pm.response.json();
pm.expect(response.data).to.have.property('id');
pm.expect(response.data.username).to.equal(username);
});
} else {
pm.test('失败注册应返回错误信息', function() {
const response = pm.response.json();
pm.expect(response).to.have.property('message');
});
}
8.3.3 JSON 数据文件
JSON 数据文件格式:
[
{
"testCase": "正常登录",
"username": "admin",
"password": "Admin123!",
"expectedStatus": 200,
"expectedRole": "admin"
},
{
"testCase": "错误密码",
"username": "admin",
"password": "wrongpassword",
"expectedStatus": 401,
"expectedMessage": "密码错误"
},
{
"testCase": "用户不存在",
"username": "nonexistent",
"password": "anypassword",
"expectedStatus": 401,
"expectedMessage": "用户不存在"
},
{
"testCase": "空用户名",
"username": "",
"password": "anypassword",
"expectedStatus": 400,
"expectedMessage": "用户名不能为空"
}
]
使用 JSON 数据:
// Pre-request Script - 使用 JSON 数据
const testCase = pm.iterationData.get('testCase');
const username = pm.iterationData.get('username');
const password = pm.iterationData.get('password');
const expectedStatus = pm.iterationData.get('expectedStatus');
const expectedRole = pm.iterationData.get('expectedRole');
const expectedMessage = pm.iterationData.get('expectedMessage');
console.log(`执行测试用例: ${testCase}`);
// 设置请求体
pm.request.body.raw = JSON.stringify({
username: username,
password: password
});
// 保存期望值
pm.variables.set('expectedStatus', expectedStatus);
pm.variables.set('expectedRole', expectedRole);
pm.variables.set('expectedMessage', expectedMessage);
pm.variables.set('testCase', testCase);
Tests Script - JSON 数据验证:
// Tests Script - JSON 数据驱动验证
const testCase = pm.variables.get('testCase');
const expectedStatus = pm.variables.get('expectedStatus');
const expectedRole = pm.variables.get('expectedRole');
const expectedMessage = pm.variables.get('expectedMessage');
pm.test(`[${testCase}] 状态码应为 ${expectedStatus}`, function() {
pm.response.to.have.status(expectedStatus);
});
if (expectedStatus === 200) {
pm.test(`[${testCase}] 应返回正确的角色`, function() {
const response = pm.response.json();
pm.expect(response.data.role).to.equal(expectedRole);
});
} else {
pm.test(`[${testCase}] 应返回正确的错误信息`, function() {
const response = pm.response.json();
pm.expect(response.message).to.include(expectedMessage);
});
}
8.4 批量执行策略
8.4.1 顺序执行
实现顺序执行:
// 使用 postman.setNextRequest 实现顺序执行
// 登录请求 Tests Script
const response = pm.response.json();
if (response.code === 200) {
pm.environment.set('token', response.data.token);
pm.environment.set('userId', response.data.user.id);
postman.setNextRequest('获取用户信息');
} else {
console.error('登录失败,停止执行');
postman.setNextRequest(null);
}
// 获取用户信息 Tests Script
if (pm.response.code === 200) {
postman.setNextRequest('创建订单');
} else {
postman.setNextRequest(null);
}
// 创建订单 Tests Script
if (pm.response.code === 200) {
const response = pm.response.json();
pm.environment.set('orderId', response.data.id);
postman.setNextRequest('支付订单');
} else {
postman.setNextRequest(null);
}
8.4.2 并行执行策略
// 并行执行多个独立请求
// Pre-request Script
const requests = [
{ name: '获取用户列表', url: '/api/v1/users' },
{ name: '获取商品列表', url: '/api/v1/products' },
{ name: '获取订单列表', url: '/api/v1/orders' }
];
const results = {};
let completed = 0;
requests.forEach(req => {
pm.sendRequest({
url: pm.environment.get('baseUrl') + req.url,
method: 'GET',
header: {
'Authorization': 'Bearer ' + pm.environment.get('token')
}
}, (error, response) => {
completed++;
if (!error) {
results[req.name] = {
status: response.code,
data: response.json().data,
time: response.responseTime
};
} else {
results[req.name] = { error: error.message };
}
if (completed === requests.length) {
pm.variables.set('parallelResults', JSON.stringify(results));
console.log('所有并行请求完成:', results);
}
});
});
8.4.3 条件执行
// 根据条件决定执行路径
const response = pm.response.json();
const userType = response.data.type;
switch (userType) {
case 'admin':
postman.setNextRequest('管理员操作');
break;
case 'user':
postman.setNextRequest('普通用户操作');
break;
case 'guest':
postman.setNextRequest('游客操作');
break;
default:
console.warn('未知用户类型:', userType);
postman.setNextRequest(null);
}
8.5 测试报告生成
8.5.1 Newman 命令行运行
# 基本运行
newman run collection.json -e environment.json
# 指定迭代次数和数据文件
newman run collection.json -e environment.json -n 10 -d data.csv
# 生成 HTML 报告
newman run collection.json -e environment.json -r html --reporter-html-export report.html
# 生成多种格式报告
newman run collection.json -e environment.json -r cli,html,json --reporter-html-export report.html --reporter-json-export report.json
# 设置超时和重试
newman run collection.json -e environment.json --timeout-request 30000 --delay-request 500
# 忽略错误继续执行
newman run collection.json -e environment.json --bail
# 详细输出
newman run collection.json -e environment.json --verbose
8.5.2 自定义报告模板
// 在 Collection Tests 中收集报告数据
const reportData = {
timestamp: new Date().toISOString(),
request: {
method: pm.request.method,
url: pm.request.url.toString(),
headers: pm.request.headers.toObject()
},
response: {
status: pm.response.code,
time: pm.response.responseTime,
size: pm.response.size()
},
tests: pm.test.results(),
iteration: pm.info.iteration + 1
};
// 保存到环境变量
const allReports = JSON.parse(pm.environment.get('testReports') || '[]');
allReports.push(reportData);
pm.environment.set('testReports', JSON.stringify(allReports));
8.5.3 测试结果导出
// Tests Script - 导出测试结果
// 在最后一个请求的 Tests Script 中执行
if (pm.info.iteration === pm.info.iterationCount - 1) {
const reports = JSON.parse(pm.environment.get('testReports') || '[]');
// 计算统计信息
const summary = {
total: reports.length,
passed: reports.filter(r => r.tests.passed > 0 && r.tests.failed === 0).length,
failed: reports.filter(r => r.tests.failed > 0).length,
avgResponseTime: reports.reduce((sum, r) => sum + r.response.time, 0) / reports.length
};
console.log('=== 测试报告摘要 ===');
console.log(`总请求数: ${summary.total}`);
console.log(`通过: ${summary.passed}`);
console.log(`失败: ${summary.failed}`);
console.log(`平均响应时间: ${summary.avgResponseTime.toFixed(2)}ms`);
// 清理环境变量
pm.environment.unset('testReports');
}
第九章:高级自动化场景实战
9.1 接口依赖处理
9.1.1 依赖链管理
实现依赖链:
// 请求1: 登录
// Tests Script
const loginResponse = pm.response.json();
if (loginResponse.code === 200) {
pm.environment.set('token', loginResponse.data.token);
pm.environment.set('userId', loginResponse.data.user.id);
console.log('登录成功,Token 已保存');
}
// 请求2: 获取用户详情
// Pre-request Script
pm.request.headers.upsert({
key: 'Authorization',
value: 'Bearer ' + pm.environment.get('token')
});
// Tests Script
const userResponse = pm.response.json();
if (userResponse.code === 200) {
pm.environment.set('userBalance', userResponse.data.balance);
}
// 请求3: 创建订单
// Pre-request Script
pm.request.headers.upsert({
key: 'Authorization',
value: 'Bearer ' + pm.environment.get('token')
});
const orderBody = {
userId: pm.environment.get('userId'),
items: [
{ productId: 'P001', quantity: 2 }
]
};
pm.request.body.raw = JSON.stringify(orderBody);
// Tests Script
const orderResponse = pm.response.json();
if (orderResponse.code === 200) {
pm.environment.set('orderId', orderResponse.data.id);
pm.environment.set('orderAmount', orderResponse.data.totalAmount);
}
9.1.2 动态依赖处理
// 动态处理接口依赖
// Pre-request Script - 检查依赖
function checkDependencies() {
const dependencies = {
token: pm.environment.get('token'),
userId: pm.environment.get('userId')
};
const missing = Object.entries(dependencies)
.filter(([key, value]) => !value)
.map(([key]) => key);
if (missing.length > 0) {
console.error('缺少依赖:', missing.join(', '));
return false;
}
return true;
}
if (!checkDependencies()) {
// 跳过当前请求,执行前置请求
postman.setNextRequest('登录');
}
9.2 Mock Server 使用
9.2.1 Mock Server 创建
Mock 响应示例:
{
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"code": 200,
"message": "成功",
"data": {
"id": 123,
"name": "测试用户",
"email": "test@example.com"
}
}
}
9.2.2 动态 Mock 响应
// Mock Server 动态响应脚本
// 在 Mock Server 的响应中使用模板
{
"code": 200,
"message": "成功",
"data": {
"id": {{randomInt}},
"name": "用户_{{randomName}}",
"email": "user_{{randomInt}}@example.com",
"createdAt": "{{now}}"
}
}
9.3 WebSocket 测试
9.3.1 WebSocket 连接
// WebSocket 连接示例
// 注意:Postman 支持 WebSocket 测试
// 在 WebSocket 请求中:
// 连接 URL
// ws://echo.websocket.org
// 发送消息
{
"type": "ping",
"timestamp": Date.now()
}
// 接收消息验证
pm.test('WebSocket 应返回相同消息', function() {
const received = pm.response.json();
pm.expect(received.type).to.equal('ping');
});
9.4 GraphQL 测试
9.4.1 GraphQL 请求配置
// GraphQL 请求示例
// 请求方法: POST
// URL: {{baseUrl}}/graphql
// Content-Type: application/json
// 请求体
const query = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
orders {
id
amount
status
}
}
}
`;
const variables = {
id: pm.environment.get('userId')
};
pm.request.body.raw = JSON.stringify({
query: query,
variables: variables
});
9.4.2 GraphQL 响应验证
// Tests Script - GraphQL 响应验证
const response = pm.response.json();
// 验证无错误
pm.test('GraphQL 响应不应包含错误', function() {
pm.expect(response.errors).to.be.undefined;
});
// 验证数据结构
pm.test('GraphQL 应返回用户数据', function() {
pm.expect(response.data).to.have.property('user');
pm.expect(response.data.user).to.have.property('id');
pm.expect(response.data.user).to.have.property('name');
});
// 验证嵌套数据
pm.test('用户订单应为数组', function() {
pm.expect(response.data.user.orders).to.be.an('array');
});
9.5 文件上传下载测试
9.5.1 文件上传
// 文件上传配置
// 请求方法: POST
// Body 类型: form-data
// 添加文件字段
// Key: file (type: File)
// Value: 选择要上传的文件
// 额外参数
pm.request.body.formdata.add({
key: 'description',
value: '文件描述'
});
// Tests Script
pm.test('文件上传应成功', function() {
pm.response.to.have.status(200);
const response = pm.response.json();
pm.expect(response.data).to.have.property('fileId');
pm.expect(response.data).to.have.property('url');
});
9.5.2 文件下载
// 文件下载测试
// 请求方法: GET
// URL: {{baseUrl}}/api/v1/files/{{fileId}}
// Tests Script
pm.test('文件下载应成功', function() {
pm.response.to.have.status(200);
// 验证 Content-Type
const contentType = pm.response.headers.get('Content-Type');
pm.expect(contentType).to.exist;
// 验证 Content-Disposition
const disposition = pm.response.headers.get('Content-Disposition');
pm.expect(disposition).to.include('attachment');
// 验证文件大小
const size = pm.response.size();
pm.expect(size.body).to.be.above(0);
console.log('文件大小:', size.body, 'bytes');
});
9.6 性能测试基础
9.6.1 响应时间监控
// 响应时间监控脚本
const responseTime = pm.response.responseTime;
const endpoint = pm.request.url.path.join('/');
// 定义阈值
const thresholds = {
'/api/v1/users': { good: 200, acceptable: 500 },
'/api/v1/orders': { good: 300, acceptable: 800 },
'/api/v1/reports': { good: 1000, acceptable: 3000 }
};
const threshold = thresholds[endpoint] || { good: 500, acceptable: 1000 };
// 评估响应时间
if (responseTime < threshold.good) {
console.log(`✓ 响应时间优秀: ${responseTime}ms`);
} else if (responseTime < threshold.acceptable) {
console.log(`⚠ 响应时间可接受: ${responseTime}ms`);
} else {
console.log(`✗ 响应时间过长: ${responseTime}ms`);
}
// 记录历史数据
const history = JSON.parse(pm.environment.get('performanceHistory') || '{}');
if (!history[endpoint]) {
history[endpoint] = [];
}
history[endpoint].push({
time: responseTime,
timestamp: Date.now()
});
// 只保留最近 100 条
if (history[endpoint].length > 100) {
history[endpoint].shift();
}
pm.environment.set('performanceHistory', JSON.stringify(history));
// 计算平均值
const avgTime = history[endpoint].reduce((sum, r) => sum + r.time, 0) / history[endpoint].length;
console.log(`平均响应时间: ${avgTime.toFixed(2)}ms`);
9.6.2 并发测试模拟
// 并发测试模拟
const concurrentRequests = 10;
const completed = [];
let finished = 0;
for (let i = 0; i < concurrentRequests; i++) {
pm.sendRequest({
url: pm.environment.get('baseUrl') + '/api/v1/test',
method: 'GET',
header: {
'Authorization': 'Bearer ' + pm.environment.get('token')
}
}, (error, response) => {
finished++;
completed.push({
index: i,
status: response.code,
time: response.responseTime,
error: error ? error.message : null
});
if (finished === concurrentRequests) {
// 所有请求完成,分析结果
const successCount = completed.filter(r => r.status === 200).length;
const avgTime = completed.reduce((sum, r) => sum + r.time, 0) / completed.length;
const maxTime = Math.max(...completed.map(r => r.time));
const minTime = Math.min(...completed.map(r => r.time));
console.log('=== 并发测试结果 ===');
console.log(`并发数: ${concurrentRequests}`);
console.log(`成功数: ${successCount}`);
console.log(`成功率: ${(successCount / concurrentRequests * 100).toFixed(2)}%`);
console.log(`平均响应时间: ${avgTime.toFixed(2)}ms`);
console.log(`最大响应时间: ${maxTime}ms`);
console.log(`最小响应时间: ${minTime}ms`);
}
});
}
第十章:生产环境最佳实践
10.1 Newman CLI 深度应用
10.1.1 Newman 完整配置
// newman.config.js
module.exports = {
collection: './collections/api-tests.postman_collection.json',
environment: './environments/production.postman_environment.json',
globals: './globals/global-variables.postman_globals.json',
iterationData: './data/test-data.csv',
iterationCount: 10,
delayRequest: 500,
timeoutRequest: 30000,
timeoutScript: 60000,
reporters: ['cli', 'html', 'json', 'junit'],
reporter: {
html: {
export: './reports/html/report.html',
template: './templates/custom-template.hbs',
title: 'API 测试报告',
showEnvironmentData: true,
showGlobalData: false
},
json: {
export: './reports/json/report.json'
},
junit: {
export: './reports/junit/report.xml'
}
},
bail: true,
suppressExitCode: false,
ignoreRedirects: false,
insecureFileRead: true
};
10.1.2 Newman 运行脚本
// run-tests.js
const newman = require('newman');
const fs = require('fs');
const path = require('path');
// 获取命令行参数
const env = process.argv[2] || 'dev';
const tags = process.argv[3] ? process.argv[3].split(',') : [];
// 加载配置
const config = require('./newman.config.js');
config.environment = `./environments/${env}.postman_environment.json`;
// 添加时间戳到报告文件名
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
config.reporter.html.export = `./reports/html/report-${timestamp}.html`;
config.reporter.json.export = `./reports/json/report-${timestamp}.json`;
// 运行测试
newman.run(config, function(err, summary) {
if (err) {
console.error('测试运行错误:', err);
process.exit(1);
}
// 输出摘要
console.log('\n=== 测试摘要 ===');
console.log(`总请求数: ${summary.stats.requests.total}`);
console.log(`成功: ${summary.stats.requests.total - summary.stats.requests.failed}`);
console.log(`失败: ${summary.stats.requests.failed}`);
console.log(`断言总数: ${summary.stats.assertions.total}`);
console.log(`断言失败: ${summary.stats.assertions.failed}`);
// 根据结果设置退出码
if (summary.run.failures.length > 0) {
process.exit(1);
}
});
10.2 CI/CD 集成
10.2.1 Jenkins 集成
// Jenkinsfile
pipeline {
agent any
environment {
POSTMAN_COLLECTION = 'collections/api-tests.postman_collection.json'
REPORT_PATH = 'reports/html'
}
stages {
stage('准备环境') {
steps {
sh 'npm install -g newman newman-reporter-html'
sh 'mkdir -p reports/html reports/json reports/junit'
}
}
stage('运行开发环境测试') {
when {
branch 'develop'
}
steps {
sh """
newman run ${POSTMAN_COLLECTION} \
-e environments/dev.postman_environment.json \
-r html,json,junit \
--reporter-html-export reports/html/dev-report.html \
--reporter-json-export reports/json/dev-report.json \
--reporter-junit-export reports/junit/dev-report.xml
"""
}
}
stage('运行测试环境测试') {
when {
branch 'release/*'
}
steps {
sh """
newman run ${POSTMAN_COLLECTION} \
-e environments/test.postman_environment.json \
-r html,json,junit \
--reporter-html-export reports/html/test-report.html \
--reporter-json-export reports/json/test-report.json \
--reporter-junit-export reports/junit/test-report.xml
"""
}
}
stage('运行生产环境冒烟测试') {
when {
branch 'main'
}
steps {
sh """
newman run ${POSTMAN_COLLECTION} \
-e environments/prod.postman_environment.json \
--folder '冒烟测试' \
-r html,json,junit \
--reporter-html-export reports/html/prod-report.html \
--reporter-json-export reports/json/prod-report.json \
--reporter-junit-export reports/junit/prod-report.xml
"""
}
}
}
post {
always {
// 发布测试报告
publishHTML(target: [
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'reports/html',
reportFiles: '*.html',
reportName: 'Postman Test Report'
])
// 发布 JUnit 结果
junit 'reports/junit/*.xml'
}
failure {
mail to: 'team@example.com',
subject: "API 测试失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "请查看测试报告: ${env.BUILD_URL}"
}
}
}
10.2.2 GitHub Actions 集成
# .github/workflows/api-tests.yml
name: API Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 6 * * *' # 每天 6:00 UTC 执行
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, test]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install Newman
run: |
npm install -g newman
npm install -g newman-reporter-html
- name: Run API Tests
run: |
newman run collections/api-tests.postman_collection.json \
-e environments/${{ matrix.environment }}.postman_environment.json \
-r html,json \
--reporter-html-export reports/report-${{ matrix.environment }}.html \
--reporter-json-export reports/report-${{ matrix.environment }}.json
- name: Upload Test Reports
uses: actions/upload-artifact@v3
if: always()
with:
name: test-reports-${{ matrix.environment }}
path: reports/
- name: Comment PR with Results
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const report = JSON.parse(fs.readFileSync('reports/report-${{ matrix.environment }}.json'));
const stats = report.run.stats;
const body = `## API 测试结果 (${{ matrix.environment }})
| 指标 | 数值 |
|------|------|
| 总请求数 | ${stats.requests.total} |
| 失败请求数 | ${stats.requests.failed} |
| 断言总数 | ${stats.assertions.total} |
| 断言失败 | ${stats.assertions.failed} |
| 平均响应时间 | ${stats.averages.responseTime.toFixed(2)}ms |
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
});
10.3 测试数据管理
10.3.1 测试数据准备
// 测试数据准备脚本
// Pre-request Script - 数据准备
async function prepareTestData() {
// 检查是否已有测试数据
if (pm.environment.has('testDataPrepared')) {
return;
}
// 创建测试用户
const userResponse = await new Promise((resolve, reject) => {
pm.sendRequest({
url: pm.environment.get('baseUrl') + '/api/v1/test/users',
method: 'POST',
header: { 'Content-Type': 'application/json' },
body: {
mode: 'raw',
raw: JSON.stringify({
username: 'test_user_' + Date.now(),
password: 'Test123!',
email: 'test@example.com'
})
}
}, (error, response) => {
if (error) reject(error);
else resolve(response);
});
});
const userData = userResponse.json();
pm.environment.set('testUserId', userData.data.id);
pm.environment.set('testUserToken', userData.data.token);
// 创建测试订单
const orderResponse = await new Promise((resolve, reject) => {
pm.sendRequest({
url: pm.environment.get('baseUrl') + '/api/v1/test/orders',
method: 'POST',
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + userData.data.token
},
body: {
mode: 'raw',
raw: JSON.stringify({
items: [{ productId: 'P001', quantity: 1 }]
})
}
}, (error, response) => {
if (error) reject(error);
else resolve(response);
});
});
const orderData = orderResponse.json();
pm.environment.set('testOrderId', orderData.data.id);
pm.environment.set('testDataPrepared', 'true');
console.log('测试数据准备完成');
}
prepareTestData();
10.3.2 测试数据清理
// Tests Script - 数据清理
// 在最后一个测试请求后执行清理
if (pm.info.iteration === pm.info.iterationCount - 1) {
// 清理测试数据
pm.sendRequest({
url: pm.environment.get('baseUrl') + '/api/v1/test/cleanup',
method: 'POST',
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + pm.environment.get('testUserToken')
},
body: {
mode: 'raw',
raw: JSON.stringify({
userId: pm.environment.get('testUserId'),
orderId: pm.environment.get('testOrderId')
})
}
}, (error, response) => {
if (!error && response.code === 200) {
console.log('测试数据清理完成');
// 清除环境变量
pm.environment.unset('testUserId');
pm.environment.unset('testUserToken');
pm.environment.unset('testOrderId');
pm.environment.unset('testDataPrepared');
}
});
}
10.4 测试报告与监控
10.4.1 自定义报告生成
// 自定义报告生成脚本
function generateCustomReport() {
const reports = JSON.parse(pm.environment.get('testReports') || '[]');
const summary = {
title: 'API 自动化测试报告',
generatedAt: new Date().toISOString(),
environment: pm.environment.name,
totalRequests: reports.length,
passedRequests: reports.filter(r => r.response.status >= 200 && r.response.status < 300).length,
failedRequests: reports.filter(r => r.response.status >= 400).length,
totalTests: reports.reduce((sum, r) => sum + r.tests.passed + r.tests.failed, 0),
passedTests: reports.reduce((sum, r) => sum + r.tests.passed, 0),
failedTests: reports.reduce((sum, r) => sum + r.tests.failed, 0),
avgResponseTime: reports.reduce((sum, r) => sum + r.response.time, 0) / reports.length,
maxResponseTime: Math.max(...reports.map(r => r.response.time)),
minResponseTime: Math.min(...reports.map(r => r.response.time))
};
summary.successRate = (summary.passedTests / summary.totalTests * 100).toFixed(2) + '%';
// 生成 HTML 报告
const html = `
<!DOCTYPE html>
<html>
<head>
<title>${summary.title}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.summary { background: #f5f5f5; padding: 20px; border-radius: 5px; }
.metric { display: inline-block; margin: 10px 20px; }
.metric-value { font-size: 24px; font-weight: bold; }
.metric-label { color: #666; }
.pass { color: #4caf50; }
.fail { color: #f44336; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
th { background: #f5f5f5; }
</style>
</head>
<body>
<h1>${summary.title}</h1>
<p>生成时间: ${summary.generatedAt}</p>
<p>测试环境: ${summary.environment}</p>
<div class="summary">
<div class="metric">
<div class="metric-value">${summary.totalRequests}</div>
<div class="metric-label">总请求数</div>
</div>
<div class="metric">
<div class="metric-value pass">${summary.passedRequests}</div>
<div class="metric-label">通过请求</div>
</div>
<div class="metric">
<div class="metric-value fail">${summary.failedRequests}</div>
<div class="metric-label">失败请求</div>
</div>
<div class="metric">
<div class="metric-value">${summary.successRate}</div>
<div class="metric-label">成功率</div>
</div>
<div class="metric">
<div class="metric-value">${summary.avgResponseTime.toFixed(2)}ms</div>
<div class="metric-label">平均响应时间</div>
</div>
</div>
<h2>详细结果</h2>
<table>
<tr>
<th>请求</th>
<th>方法</th>
<th>状态码</th>
<th>响应时间</th>
<th>断言结果</th>
</tr>
${reports.map(r => `
<tr>
<td>${r.request.url}</td>
<td>${r.request.method}</td>
<td class="${r.response.status < 300 ? 'pass' : 'fail'}">${r.response.status}</td>
<td>${r.response.time}ms</td>
<td>${r.tests.passed}/${r.tests.passed + r.tests.failed}</td>
</tr>
`).join('')}
</table>
</body>
</html>
`;
// 保存报告(在实际使用中需要写入文件)
pm.environment.set('customReport', html);
console.log('自定义报告已生成');
}
// 在测试结束时调用
if (pm.info.iteration === pm.info.iterationCount - 1) {
generateCustomReport();
}
10.5 团队协作最佳实践
10.5.1 Collection 版本管理
项目结构:
├── collections/
│ ├── v1.0/
│ │ └── api-tests.postman_collection.json
│ ├── v1.1/
│ │ └── api-tests.postman_collection.json
│ └── latest/
│ └── api-tests.postman_collection.json
├── environments/
│ ├── dev.postman_environment.json
│ ├── test.postman_environment.json
│ └── prod.postman_environment.json
├── data/
│ ├── test-data.csv
│ └── test-data.json
├── reports/
│ ├── html/
│ ├── json/
│ └── junit/
├── scripts/
│ ├── run-tests.js
│ └── newman.config.js
└── README.md
10.5.2 命名规范
Collection 命名规范:
- 项目名称-API名称-版本
- 示例:UserAPI-用户管理-v1.0
Request 命名规范:
- 模块-操作-描述
- 示例:用户-登录-正常登录
Folder 命名规范:
- 按模块或功能分组
- 示例:用户管理、订单管理
Environment 命名规范:
- 项目-环境
- 示例:UserAPI-Dev、UserAPI-Prod
10.5.3 文档维护
# API 测试文档模板
## 概述
- 测试范围
- 测试环境
- 执行方式
## 前置条件
- 环境变量配置
- 测试数据准备
## 测试用例
| 用例编号 | 用例名称 | 请求方法 | 路径 | 预期结果 |
|---------|---------|---------|------|---------|
| TC001 | 用户登录 | POST | /api/login | 200 |
## 执行命令
\`\`\`bash
newman run collections/api-tests.postman_collection.json -e environments/dev.postman_environment.json
\`\`\`
## 报告查看
- HTML 报告:reports/html/report.html
- JSON 报告:reports/json/report.json
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)