接口测试 Postman 做接口自动化测试-2. 常用代码片段

Keith Mo · 2016年12月09日 · 最后由 rockyrock 回复于 2018年01月25日 · 2089 次阅读

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[`foo为string类型(实际值:${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

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 14 条回复 时间 点赞

PS:

有人接盘了,超开心~:P

这篇没什么干货,依然还没说到命令行的 Newman,就是方便复制粘贴用的

数据驱动测试这里没提,一开始就是搞了一些数据文件,用了一段时间,现在全砍掉了

这块等想通了再补充

如果做不到维护方便、有意义和误报率低,不如不要

下一篇是 Newman,加上新写的 shell 脚本

(毕竟太难解释为啥专门写几个 node 脚本做这做哪了,何况最有价值的把 newman 跑出的各种数据存进 db 这块还没开工……
把主要逻辑简化下写成 shell 脚本更好理解)

15楼 已删除

#1 楼 @keithmork 接盘是啥意思?

#3 楼 @Lihuazhang 当接盘侠😝

接口这块不用全塞给我,跟业务相关的他们自己测,这就有更多时间忙别的事了

Keith Mo Postman 做接口自动化测试-1. 入门篇 中提及了此贴 12月09日 21:46

JSON Schema 有在线生成器:http://jsonschema.net/

#7 楼 @ike 最后的链接里的生成器也是这个,有生成器也省不掉查资料熟悉语法的时间,毕竟要删除替换和添加一些东西

如果接口够稳定,比如是发布出去的 SDK,结构全都不能变,麻烦一次稳定运行几个月也还行

如果是自己用的,天天改,只写几个关键的断言就算了

  • 被测接口如果功能不稳定,需求不明确,经常改的话,维护工作量太大

😂 找到组织了,项目在初期,迭代特别快,刚开始还会写点 schema,但后来实在没法维护,每天改 case 占了大半时间。。。
目前 collection 总数到了 106,最长的里面 request 有 20 个,总 test 个数也高达 1100,自动化我是用 nodejs 包 newman 放到 jenkins 上做的,同步跑下来要 20 分钟。。。
现在老板要解决最最头痛的问题,postman 的复用性,楼主有没有啥好方向啊?在看 postman-collection 的 sdk,感觉也是深坑一个啊。。。

#9 楼 @yusufchang 我也是 nodejs 包 newman,以前异步跑不到 20 秒(输出到控制台的 log 混一起是个大坑,暂时没想好是改同步还是写文件,在自己的框架成型前退回去用 shell 脚本同步跑了……)

我的测试没你多,不到 300,很重要很稳定的才加入,不爽的踢走

复用性真正要解决得等新版本,据说能在集合层面复用代码

全局的复用我暂时是把公共函数压缩了塞 globals 里,eval() 加上复制粘贴勉强用着

之前在 nodejs 里写好了从外部 js 文件读入内容,转义后找到 collection 文件里的某些标志替换掉

例如 postman 里注释写//use:foo,node 脚本就会去读放在指定目录下的foo.js,替换掉这行注释

用了一段时间,暂时跟 data file 一起扔掉了,以后再看情况捡起

这方法只适合用于整个项目每个接口都必须做而且完全一样的断言,不适合 pre-request script 和提取变量,不然等于放弃了用界面发请求调试

#10 楼 @keithmork 非常感谢楼主的回复。

异步的话,用到了 nodejs 的 async 模块,跑下来也差不多 5 分钟,不过经常会有些奇怪的问题,可能跟项目也有关系。至于 log 的话,因为用了 child_process 的 exec 方式去 run 的 newman,没出现什么问题,每个 collection 的日志基本都长这样:

恩恩,确实 case 多起来,改起来实在不爽,但是因为我们是前端 team,不放心后端的 api 质量,所以才这么干的。基本上公共的方法也是采用 eval 这种写到 env 里的,可读性确实太差了。

你上面提到的这种方法,听起来还不错,目前就在看 postman collection 的 sdk,初步想法就是抛弃 postman app,用习惯的 IDE 去写 collection,不过感觉还是有一些难以逾越的问题,比如我们的 api 逻辑性蛮强,可能一个 create 的操作需要依赖前面 n 个 basic 的 bo 的 id,怎么定义更通用基础 collection 很头疼。另外舍弃了 postman app 的话,就做不到所见即所得了,感觉得不偿失啊。。。

话说 postman 新版本支持复用代码,楼主有什么更详细的消息么?大概能做成啥样?时间知道么?

谢谢~~

#11 楼 @yusufchang exec 看起来像是我想要的,谢谢:)

新版本不知道什么时候出具体会搞成怎样,之前给 newman 提 issue 的时候作者提到的,反正我是不抱希望……

newman 缺的东西太多了

postman 最大的优点就是易用,人手一个

而 newman 就是个残废,连批量执行、跳过某些文件、外部脚本、标签、自动关联数据文件和拿到数据文件里的用例数这些东西都要你自己实现,代码重用和流程控制跟没有没什么分别……

要不是 postman,我还不如连 http 请求都自己写,扔掉它算了……

有 postman 在,谁定义了啥接口,他自己用来调试的用例可以导出到处发,方便联调

要是适合做自动化,改一改加上断言就搞定

甩锅也容易,到底是参数传错还是接口有问题一看就知道

另外,让做手工测试的童鞋自己写业务相关的接口测试理论上效率比较高

PS:图中接口的响应时间有点长啊……难怪跑这么久
PS2:我基本都在测后端,很能理解你的感受,特别是那些薛定谔的 bug,天天在烦你,谁观测它它就消失了😆

#12 楼 @keithmork 握爪,简直不能同意的更多😭 ,postman 的好处真心太过于明显,前后端无论开发测试全用,随便有个问题丢过去就能查,既直观又便利。。。

但是硬伤和你说的一样,newman 确实还是有些弱;但是离开 postman 似乎对于我们又显得不太可能,毕竟还有那么多 case 在那呢,简直就是食之无味,弃之可惜啊。。。

响应时间这个还算好的,资源受限,cd 团队只能给到这样的测试机器,而且还是很多 namespace 共用的环境,心里苦啊。。。

没办法,case 复用性太差,改起来太痛苦,但看来也只能这条路走到黑了😂

#13 楼 @yusufchang 要是真的有所有用例完全一样的代码块要改还好,sed 替换掉,就烦各种小地方有那么一丁点不一样的那些😟

刚学习 postman,请教一个问题:const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; 这一行在全局变量里如何设置呢?在 pre-request script 里如何调用呢?
谢谢!!

你好,想要用用户名密码登录后,获取 cookie,然后把 cookie 放在其他接口的 header 里,这个该怎么获取登录接口生成的 cookie 呢?

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册