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~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,试着在项目用了下,效果不好

// 依然假设返回的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(通常默认选项就够用了)

Tiny Validator

JSON Schema


↙↙↙阅读原文可查看相关链接,并与作者交流