Postman 自动化测试备忘

最近尝试了下使用Postman中的Test对API进行自动化测试,感觉用起来很方便,就在本文中记录一下Postman API测试的构建方法。

本文并不是针对Postman新手的文章,需要对Postman有一定了解,这里假定您已经使用过Postman进行简单的API调试,并拥有一定JavaScript、Nodejs和前端工程化经验。

确定所有API的顺序性

从标题上可以比较难理解,翻译一下就是用户在前端上面的操作从最开始的登录,注册再到列表查询,再到业务请求等整个对API请求的顺序流程。这个顺序流程主要为了帮助我们理清楚API之间的相互依赖关系,下面就例举一下最常见的一些依赖关系:

  • 所有需要权限的API ==依赖于 ==> 登录API(其返回值中的AccessToken等令牌)
  • 特定某个实体API ==逻辑上(可能)依赖于 ==> 该实体列表查询API
  • 某个业务API ==逻辑上(可能)依赖于 ==> 某一实体API(其uuid)或 另一业务API(其返回请求ID,例如订单id等)

上面这一张图简单的说明了一些常见API的依赖关系,可以看到几乎所有API都依赖于Auth登录鉴权API所提供的Access Token,而实体详细情况API则都依赖于前面的实体列表API(可能也有其他实体中的关联ID),这样我们就可以比较清晰的了解到整个API体系中的各种依赖关系,进而方便的在Postman的工作空间中通过文件夹将API分割为不同的部分,达到将公共依赖提取出并使用公共测试、公共pre-request脚本的方法减少代码冗余,并统一管理复用同义代码。

在确定好API的顺序性之后就可以非常方便的将API之间的依赖关系抽离出来了。

## 案例

这里以一组简单的API来展示一下整个自动化API测试的过程:

  • POST /api/register 请求体 { account:'hello',password:'world'} 格式json。
  • POST /api/auth 请求体 { account:'hello',password:'world'} 格式json。返回 { access_token:'asdadassdasgsd'} 格式数据
    • 使用app_key、app_secret获取access_token令牌用于后续受保护API访问。
  • GET /api/articles?access_token=xxxxxx 请求体通过 { pagination:{ page:1,size:5} } 的json格式进行分页请求
    • 获得所有文章
  • GET /api/articles/{article_id}?access_token=xxxxxx
    • 获得某一文章详细内容
  • POST /api/articles/{article_id}?access_token=xxxxxx
    • 获得某一文章详细内容
  • PUT /api/articles/{article_id}?access_token=xxxxxx
    • 获得某一文章详细内容
  • DEL /api/articles/{article_id}?access_token=xxxxxx
    • 获得某一文章详细内容

简易代码

这里提供一个简单的测试代码:

代码使用express框架写了这几个简单的API,校验就没用再复杂化了,直接做的简单的服务器session类的access-token,数据使用faker-js 进行随机生成。代码我上传到了Github上

import { faker } from '@faker-js/faker';
import * as express from 'express';

// DATA
let access_tokens:{token:string,expire:Date,account:string}[] = [];
const accounts:{account:string,password:string}[] = [];
interface Pagination{
  page:number;
  size:number;
}
interface PaginationRequestModel{
  pagination?:Partial<Pagination>;
}
interface Article{
  title:string;
  id:number;
  content:string;
  author:string;
}
let articles:Article[] = [];
let article_amount:number = 0;
let MAX_ARTICLE_ID = 0
let auto_gen_articles = 10;

while(auto_gen_articles-- > 0){
  article_amount++;
  articles.push({
    id:auto_gen_articles,
    title:faker.lorem.text(),
    content:faker.lorem.paragraphs(5),
    author:`${faker.name.firstName()}${faker.name.lastName()}`
  })
  MAX_ARTICLE_ID++;
}

const app = express();
app.use(express.json())

app.get('/api', (req, res) => {
  res.send({ message: 'Welcome to auto-api-test!' });
});

const port = process.env.port || 3333;
const server = app.listen(port, () => {
  console.log(`Listening at http://localhost:${port}/api`);
});

//Anonymous API
app.post('/api/register',(req:express.Request<any,any,{account:string,password:string}>,res)=>{
  let account = accounts.find(a=>a.account==req.body.account);
  if(account){res.status(409);res.send();return;}
  accounts.push(req.body);
  res.status(200);
  res.send();
  return;
})
app.post('/api/auth',(req:express.Request<any,any,{account:string,password:string}>,res)=>{
  let account = accounts.find(a=>a.account==req.body.account && a.password == req.body.password);
  if(account){
    let access_token =faker.datatype.hexadecimal(32);
    access_tokens.push({token:access_token,expire:(new Date(Date.now()+360000)),account:account.account})
    res.json({access_token})
    res.status(200);
    res.send();
  }
  res.status(400);
  res.send();
})

//Authorized API
const router = express.Router();
const auth:express.Handler = (req:express.Request<{access_token:string}>,res,next)=>{
  if(req.query.access_token){
    let session_token = access_tokens.find(a=>a.token == req.query.access_token);
    if(!session_token){
      res.send(400);
      return;
    }
    if(session_token.expire.getTime() > Date.now()){
      res.locals.user = session_token.account;
      next();
      return;
    }
    else{
      access_tokens = access_tokens.filter(a=>a.token != session_token.token);
      res.send(498)
    }
  }
  res.status(400);
  res.send()
}

router.get('/api/articles',auth,(req:express.Request<any,any,PaginationRequestModel>,res)=>{
  let pagination:Pagination = Object.assign({},<Pagination>{page:1,size:5},req.body.pagination);
  pagination.page-=1;
  if( pagination.page < 0 || pagination.size <= 0 || pagination.page * pagination.size >= article_amount){res.json([]);return;}
  let page_index = pagination.page * pagination.size;
  let paged_articles = articles.slice(page_index,page_index + pagination.size);
  res.json(paged_articles);
  return;
})
router.get('/api/articles/:id',auth,(req:express.Request<{id:string}>,res)=>{
  let article =articles.filter(a=>a.id == Number(req.params.id));
  if(article.length == 0){
    res.status(404);
  }
  res.json(article[0]);
})
router.post('/api/articles',auth,(req:express.Request<any,any,{article:Omit<Article,'id'|'author'>}>,res)=>{
  let article = req.body.article as Article;
  if(article == undefined || article.content == undefined || article.title == undefined){
    res.status(400);
    res.send({ message:'missing parts'});
    return;
  }
  article.author = res.locals.user;
  article.id = MAX_ARTICLE_ID++;
  articles.push(article);
  res.send({message:'success',article})
})
router.put('/api/articles/:id',auth,(req:express.Request<any,any,{article:Article}>,res)=>{
  articles = articles.filter(a=>a.id != Number(req.params.id));
  articles.push(req.body.article);
  res.send({message:'success'})
})
router.delete('/api/articles/:id',auth,(req:express.Request,res)=>{
  articles = articles.filter(a=>a.id != Number(req.params.id));
  res.send({message:'success'})
})


app.use(router);

server.on('error', console.error);

API接口测试简述

Postman接口测试使用方法是通过将接口按依赖顺序从上到下的排列,并通过pre-request script 和 Test 两种方式将依赖数据填写到对应的环境变量中,完成整个依赖链的补全,并通过在test中设置setNextRequest将默认从上到下的执行流程更改以达到各种测试目的。而测试方面主要在Test中设置各种变量值测试用例及Postman内置的各种Assert方法实现。

下面是一个Postman请求执行的过程,看一看到请求执行之前有一个pre-request sciprt请求预执行脚本,而在拿到response后又执行了一个test script,从命名上也可以看出他们的主要用途。前者用于执行请求之前的一些参数判断等逻辑,而后者则执行对响应结果的一些测试。

下方文本编辑框中就是对某一API的响应的简单测试和一个环境变量设置操作。可以看到这里使用的是JavaScript编写测试脚本,而右侧边栏则是一些Postman预制的脚本和测试案例,通常情况下直接使用的这些测试案例就足够日常使用了。

文件夹 Folder

文件夹是Postman中将同一类别的API规整到一起的方法,通过将同一权限、同一类型的API放置到同一文件夹下,可以提高整个API测试项目的可读性和结构性。同时Folder也是后续测试脚本编程不可缺少的一部分,在Folder上我们可以添加该文件夹下所有API都依赖的一些请求预执行脚本和测试脚本,减少代码冗余。

上图可以看到我将整个项目分成了三个文件夹,一个Artcles相关的API文件夹,一个和鉴权校验相关的Authentication文件夹,一个基础测试的Basic Test文件夹。

集合 Collections

集合是Postman中一个将同一项目的API规整到一起的方法,在Collections上可以执行Collection Runner,将整个项目的API执行起来,完整整个API自动化测试的触发。

同时上图右上角有一个Watch按钮和一个Fork按钮,可以非常简单的联想到版本管理工具,而这实际上也是Postman内置的对于API测试集合的一个版本化管理快捷按钮,可以直接对当前Collection进行Fork和对项目变更进行Watch。

变量 Variables

Postman中环境拥有两个主要层级,Gloabl、Environment,作用域小的同名变量值会覆盖作用域大的同名变量值。

  • Global作用域的变量在所有范围内始终是可用的一组变量。
  • Environment级别的变量合集可以存在多个,但同一时间只能有一组Environment变量被启用。

而不同请求之间即可使用Global或Environment变量进行跨请求数据传递,这也是我们上面所讨论的依赖数据的传递方法。

变量的手动增加和修改时在Postman左侧Environments菜单栏中,这里可以看到Global级别的变量实际是一直处于启用状态的,而default环境变量则后面有一个勾选标志,这个标志就表示当前状态下是将default环境变量组启用。

右侧则是对default环境变量组中变量的设置,这里有一个access_token变量,它的current_value已经被设置,但它的初始值留了空。非常简单的一个默认值和当前值设置的一个表格栏。

而在请求中使用变量有两种办法,一种是在请求预执行脚本或测试脚本中使用JavaScript获取,第二种是通过在界面上使用{{VARIABLE_NAME}}的方式自动获取。后面讲主要介绍更加灵活的JavaScript方式获取变量,需要注意的是这里使用{{VARIABLE_NAME}}获取的变量是该变量名作用域最小变量组的值。

请求预执行脚本 Pre-request Script

请求预执行脚本是在当前请求发出前执行的一段JavaScript代码,这段代码中可以使用Postman提供的API对Postman的环境等参数进行请求定制化的设置。

上图中是一个对于Article的列表获取方法,一个对/api/articles的 GET 请求,这个请求需要在查询字符串中带入正确的access_token才能访问,因此这里将会在环境变量中寻找access_token,然后将其值添加到QueryParams中。这里的pm是Postman的缩写,也是Postman在所有的Postman脚本中提供的一个对于Postman各种参数操作API的一个对象接口。

具体的各种API见:Postman Test Script Documentation

在我们这个例子中实际上请求预执行脚本做的事情非常简单,就是单纯的将环境变量中的access_token填入查询字符串参数中,以通过权限校验。

结果测试脚本 Result Test Script

结果测试脚本,顾名思义是对请求结果的一个测试,但在这里也可以对环境变量等参数进行变更,因此结果测试脚本在我们的案例里面实际做了两件事。

  • 第一个是在auth(登录校验接口)的response body中提取access_token并放进Environment中。
  • 第二个是对一些返回值进行JSON校验,以保证其正确性。

上图中代码第一部分为一个名为Whether access_token exist的测试,只要写过Jest等其他测试框架的测试用例都应该对这种方式不陌生。然后下面那一行应该也可以通过语义读出来,就是一个简单的json值获取然后填到environment的过程。只要这个Auth请求在其他需要access_token 的请求之前执行,就可以让其他依赖access_token的请求可以通过环境变量拿到其值。

运行整个接口测试项目

选择整个Collection,然后在右上角找到Run按钮就可以进入集合测试运行界面。

在运行整个项目之前需要将各个文件夹按照依赖顺序排列正确,上图中的排列顺序就是有有问题。由于Postman在执行整个接口测试项目时默认的执行顺序是从上往下从顶层到底层的顺序执行,因此上图中首先将执行的是需要access_token的Articles资源的CRUD API,当其执行时环境变量中根本就没有access_token值,因此无法正确执行。

这里我对所有的返回结果都判定了下Status Code 是否为200,我在后端代码中只要检测到没有access_token,返回码就是400 Bad Request。可以看到所有需要access_token的请求实际都鉴权失败了。

而我们将文件夹顺序调整一下又是完全相反的结果:

所以在使用默认的Postman集合测试条件下,将整个项目中API项目按照依赖顺序排列起来是一个必要条件。

但Postman实际上提供了在Test中更改后续执行的请求的API:postman.setNextRequest("request_name") 来对后续执行的请求顺序进行更改,详细请查阅Postman文档。

将公共Pre-request、Result Test脚本提取

在上面介绍的这些脚本编写方法只是针对单个Request,但现实场景下很多请求都具有一些相同的依赖,例如上述的所有Articles API实际上都依赖于access_token变量,这里如果我们直接在每一个Article API的请求预执行脚本添加一个access_token获取代码就非常的不合理,在代码量比较小的时候还比较简单,但如果想要真正当成一个API测试项目,这种方法肯定会产生大量冗余代码。而在Postman中这种公共代码的正确处理办法是在Collection或者Folder上添加公共代码,这样其下的子请求都会继承这些代码,在请求预执行阶段和结果测试阶段就会自动先从父级文件夹和集合上执行,而后执行请求自身的脚本。

总结

总的来说使用Postman进行API自动化测试的设置和操作是比较简单的,Postman也提供了一些其他API开发相关的功能,例如API文档(通过OpenAPI规范配置)、Mock服务器(通过API文档生成Mock API)、API监控(对某些API进行定时调用监控其状态)、代理服务(捕捉本机上所有请求,感觉还是Charles、Wireshark更好用)。但总体来说还是API测试方面更加符合我当前的应用场景,因此这些功能就不再去赘述了。

参考文章

  • https://medium.com/better-practices/from-manual-to-automated-testing-the-roadblocks-and-the-journey-6333dfacc5ae
  • https://learning.postman.com/docs/writing-scripts/intro-to-scripts/
  • 《接口自动化测试持续集成:Postman+Newman+Git+Jenkins+钉钉》 Storm

  • 2022-06-14 wep 完成文章第一部分撰写
  • 2022-06-18 aep 添加测试一部分内容
  • 2022-06-19 aep 完成文章编写

发表评论

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