1. STest软件测试社区首页
  2. 后台测试

Postman做接口自动化测试

2017-12 补充:Postman某次大更新后API有了非常多不兼容的改动,这下面有些肯定调不通了

这里假设你对Postman比较熟悉

如果之前没关注过,可以看 上一篇


如果不打算用来做自动化测试,以下绝大部分都没必要

对随便在界面发下请求来说太麻烦


Tests常用函数

假设接口返回JSON

前提:globals里存了常用的函数,如:

assertNotTimeout:var hasResponse=postman.getResponseHeader('Content-Type')?true:false; if(!hasResponse) tests['服务端在超时前没返回任何数据,请检查相关服务、网络或反向代理设置(以下跳过其他断言)']=false;
logParams:if(hasResponse) tests[`[INFO] 请求参数(仅限POST,超时没返回时不解析):${JSON.stringify(request.data)}`]=true;
getResponseJson:try{if(hasResponse) var json=JSON.parse(responseBody);}catch(err){ tests['服务端没返回合法的JSON格式,请检查相关服务、网络或反向代理设置(以下跳过其他断言)']=false; tests[`[INFO] 返回:${responseBody}`]=true; console.error(err);}
assertType:var assertType=(name,value,type)=>{let isType=(type==='array')? Array.isArray(value):typeof value===type; tests[`${name}为${type}(实际值:${value})`]=isType;};
assertEqual:var assertEqual=(name,actual,expected)=>{tests[`${name}等于${expected}(实际值:${actual})`]=actual===expected;};
assertNotEqual:var assertNotEqual=(name,actual,expected)=>{tests[`${name}不等于${expected}(实际值:${actual})`]=actual!==expected;};

这块难看不是太大问题,1个人写好、导出、提交Git,通知其他人拉、导入,完事


然后就可以一种套路走天下——

开头把函数全拿出来:

// setup
eval(globals.assertNotTimeout); // 判断是否超时
eval(globals.logParams); // 在报告里显示POST请求参数
eval(globals.getResponseJson); // 返回json变量,保存了整个返回的JSON对象
// 定义几个常用函数,参数都是 显示文字、实际值、期望值
eval(globals.assertType);
eval(globals.assertEqual);
eval(globals.assertNotEqual);

假设项目里每个接口必定会带resultCode属性,成功返回1,

失败时还会带resultMsg detailMsg属性,但未必2个都有提示信息

先来个通用的套路,不管接下来漏什么,总不会把最基本的漏了:

// 项目通用的断言
if (json) {
const { resultCode, resultMsg, detailMsg } = json;
assertEqual('resultCode', resultCode, 1);

if (resultMsg) tests[`[INFO] 接口提示信息:${resultMsg}`] = true;
if (detailMsg) tests[`[INFO] 接口提示信息:${detailMsg}`] = true;
}

如果这接口报错也是正常情况(如检查手机号是否已注册的接口,毕竟随机生成的号码有可能跟已有的重复),加上流程控制,反复通过名字调自己,直到得到想要的结果

if (!resultCode || resultCode !== 1) {
tests[`[WARN] 手机号${environment.randomMobile}无法注册,重试中……`] = true;
postman.setNextRequest('验证手机是否注册');
}

如果这接口执行失败会导致之后的接口没有跑的意义,那就中止测试流程

(比如注册失败,接下来需要登录的操作都不用测了)

// (注明中止理由)
if (!resultCode || resultCode !== 1) {
tests['[ERROR] 执行失败,跳过依赖本接口的后续测试'] = false;
postman.setNextRequest(null);
}

在界面/HTML报告里看到的assertEqual输出例:resultCode等于1(实际值:1)

有些接口返回内容很简单,不需要做什么验证,到上面就结束了

门槛低到只要会复制粘贴就“能做自动化测试”(shell脚本别人写,Jenkins别人配),还很稳定!


当然,全都像上面这样搞的话会有很多假阴性

很多接口还要断言更多东西

如果返回值需要和环境变量/请求参数/固定的值做对比,与其写得到处都是,将来改起来忘了这忘了那

不如都塞一个对象里,要改一次改完:

// 该接口的断言
const expected = {
mobile: environment.PATIENT_MOBILE,
userType: 1,
};

// 如果值来自写死的请求参数:request.data.变量名
// 如果值来自数据文件:data.变量名
// Postman还是很体贴的,给了你各种实用的全局对象(虽然在文档里藏得很深,虽然那个data极易重名……)

假设项目里大多数接口返回的JSON里都有个data对象,各种业务相关属性都在里面

接下来又一个套路:

if (json && json.data) {
// ...
} else {
tests['返回值包含data对象'] = false;
}

if里断言什么提取什么就跟具体接口有关了,需要了解业务逻辑、查接口文档、问开发等

但还是有不少套路

首先总要把某些属性取出来放进变量吧(这里用了ES6的对象解构)

const { access_token, patientId, userId, login, user } = json.data;

需要放进环境变量传给下个请求用的东西,总得做点断言吧,就算没法断言具体的值,判断类型还是可以的

assertType('令牌', access_token, 'string');
assertType('患者ID', patientId, 'number');
assertType('user', user, 'object');

environment.patientId = patientId;
environment.PATIENT_ACCESS_TOKEN = access_token;

返回的某个对象里如果需要做更精细的断言,继续拆

上面我们定义的expected对象在这里用上了

if (user) {
const { telephone, userId, userType, } = user;
assertEqual('mobile', telephone, expected.mobile);
assertEqual('userId', userId, json.data.userId);
assertEqual('userType', userType, expected.userType);
}

PS:

专门用难维护的globals定义函数的意义就在这里,断言数量太多了(上面省略了一些)

如果用官方的写法,想想满屏差不多又有点不同的东西维护起来多恐怖……

tests[`foostring类型(实际值:${foo})`] = typeof foo === 'string';
tests[`bar等于1(实际值:${bar})`] = bar === expected;

【总结】

写断言的套路不止1个,但个人认为这个比较适合小公司/小项目(最底下还有个JSON Schema的)

上面的代码块连起来搞成一个可以复制粘贴的模板就能到处用

等以后新版本出来了,支持在集合/文件夹级别定义函数,可能就没必要到处复制粘贴,甚至不需要globals


Pre-Request Script常用函数

当前时间戳

有些接口,如拉消息,返回比提交的时间戳新的数据

// 如果不打算重用,在参数里用Postman的内建变量`{{$timestamp}}`就行,否则:

environment.ts = Date.now();
  • 如果本地/测试服务器和应用服务器时间不同步会影响结果,可以考虑加减一定毫秒数
  • 对时间非常敏感的接口可能不适合用自动化的手段验证,要仔细选取用例/场景,不能为做而做

UUID

// 如果不打算重用,在参数里用Postman的内建变量`{{$guid}}`就行,否则:

const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
.replace(/x/g, () => (Math.floor(Math.random() * 16)).toString(16))
.replace(/y/g, () => (Math.floor(Math.random() * 4 + 8)).toString(16));
  • 这写法比较易懂,在现在的机器上跑只需要0-1ms,足够了
  • 更多讨论见 stackoverflow的帖子

随机

让请求参数有点变化

// 如果想要0~1000的随机数,且不打算重用,参数里直接用Postman内建变量`{{$randomInt}}`就行
// 否则自己实现:

const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; // 随机整数
const getRandomValue = list => list[randomInt(0, list.length - 1)]; // 随机选项

例:

// 随机手机
environment.randomMobile = `18${randomInt(100000000, 999999999)}`;
// 随机2-6字姓名
const charsInName = ['赵', '钱', '孙', '李', '王', '张'];
const numOfChars = randomInt(2, 6);
let randomName = '';
for (let i = 0; i < numOfChars; i++) {
let index = randomInt(0, 5);
randomName += charsInName[index];
}
environment.randomName = randomName;
// 随机设备token(推送服务商提供)
const chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
let deviceToken = '';
for (let i = 0; i < 64; i++) {
deviceToken += getRandomValue(chars);
}
environment.randomDeviceToken = deviceToken;
// 随机设备名
environment.randomDevice = getRandomValue(['ios', 'android']);
// 随机行政区划
const divisions = ['北京市', '上海市', '天津市', '重庆市', '广东省 深圳市', '广东省 广州市', '新疆维吾尔自治区 克孜勒苏柯尔克孜自治州'];
environment.randomDivision = getRandomValue(divisions);
// 随机生日(时间戳)
// 假设今天是2017-1-1,距1970-1-1 47年,则生日范围为 1923-1-1 ~ 2017-1-1
environment.randomBirthday = randomInt(0 - Date.now(), Date.now());
// 随机群名
const groupNames = ['犯罪团伙', 'We are gay', '`~!@#$%^&*()-_ =+'];
environment.groupName = getRandomValue(groupNames) + randomInt(0, 1000);

环境变量未定义就赋初始值

environment.XXX == null || environment.NAME = value;
// == null 匹配 null 和 undefined
// 通常避免 !environment.XXX 或 environment.XXX || ... 的写法,变量有可能是false, '', 0

同步等待

避免发送请求的速度比数据库更新速度快,造成误报

const sleep = (milliseconds) => {
const start = Date.now();
while (Date.now() <= start + milliseconds) {}
};

// 就是限时的死循环,请用小一点的数字调试
// Postman是用JS写的,单线程异步,主线程被阻塞了就没法做其他操作

修改密码

用于修改密码接口,2套密码来回替换

// 假设已设置了环境变量PWD

const oldPwd = environment.PWD;
const newPwd = environment.NEW_PWD;

if (!newPwd || newPwd === oldPwd) {
newPwd = '123456';
} else {
const tmp = oldPwd;
oldPwd = newPwd;
newPwd = tmp;
}

environment.NEW_PWD = newPwd;
environment.PWD = oldPwd;

(备忘)用JSON Schema校验格式

仅仅因为Postman支持tv4,试着在项目用了下,效果不好

  • 哪来那么多时间写(这个看项目)
  • 太长了,在Postman里拉几屏看不全,不好维护
  • 通用的不通用的断言和异常处理都在一块,改起来容易漏
  • 别人一看就吓跑,拉不到人入坑
  • 被测接口如果功能不稳定,需求不明确,经常改的话,维护工作量太大
// 依然假设返回的JSON里有个字段叫resultCode,1表示成功
// (当时还没想到用globals存函数,中间反序列化JSON那段基本上就是现在的globals.getResponseJson)

const schema = {
// 这里手写/贴上在线工具生成的一长串JSON schema
}

let json;
try {
json = JSON.parse(responseBody);
} catch(err) {
tests['服务端没返回合法的JSON格式,请检查相关服务、网络或反向代理设置(以下跳过其他断言)'] = false;
tests[`[INFO] 返回:${responseBody}`] = true;
console.error(err);
}

if (json) {
const result = tv4.validateResult(json, schema);
tests['JSON Schema格式正确'] = result.valid;

if (result.valid) {
tests.isSuccess = json.resultCode === 1;

if (tests.isSuccess) {
// ...
}
} else {
console.error(result.error);
console.error(responseBody);
}
}

JSON Schema可用 这网站 生成,把返回的JSON字符串贴进去,点Generate Schema(通常默认选项就够用了)

  • 注意不要复制"$schema":那行,Postman不支持引用外部模板
  • 按默认参数,会把贴进去的JSON里的所有字段都认为是必须的。如果某些返回字段是可选的,找到相应的"required":数组,去掉那字段

Tiny Validator

JSON Schema

,

原创文章,作者:AIOTest,如若转载,请注明出处:https://www.stest.com

发表评论

邮箱地址不会被公开。 必填项已用*标注

QR code