热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Node.js躬行记(26)——接口拦截和页面回放实验

原标题:Node.js躬行记(26)——接口拦截和页面回放实验最近在研究Web自动化测试,之前做了些实践,但效果并不理

原标题:Node.js躬行记(26)——接口拦截和页面回放实验

  最近在研究 Web自动化测试,之前做了些实践,但效果并不理想。

  对于 QA 来说,公司的网页交互并不多,用手点点也能满足。对于前端来说,如果要做成自动化,就得维护一堆的脚本。

  当然,这些脚本也可以 QA 来维护,但前提是得让他们觉得做这件事的 ROI 很高,依目前的情况看,好像不高。

  所以在想,做一个平台,在这个平台中可以保存些数据,并且在旁边提供个小窗口,呈现要测试的 H5 网页,如下图所示(画图工具是excalidraw)。

  在修改相关数据后,可以直接看到网页的变化。

  

  QA 或前端可以不用再写脚本代码,就能实现自动化测试。

  目前想到两块,第一块是拦截请求,mock 响应;第二块是记录页面行为,然后自动回放,最后截图,和上一次的截图做对比分析,看是否相同。

一、拦截请求

  拦截请求就是将响应 mock 成自己想要的数据,然后查看页面的呈现。

  这样就能模拟各种场景,毕竟测试环境的业务数据肯定不能满足所有场景,所以需要自己造。

  有了平台后,就能将造的数据保存在数据库中,可随时调取查看页面呈现。

1)拦截

  现在就要实现拦截,我首先想到的就是注入脚本,然后在 XMLHttpRequest 或 fetch() 埋入拦截代码。

  以 XMLHttpRequest 为例,在 monitorXHR() 函数中就可以让请求转发到代理处。

var _XMLHttpRequest = window.XMLHttpRequest; // 保存原生的XMLHttpRequest
//
覆盖XMLHttpRequest
window.XMLHttpRequest = function (flags) {
var req = new _XMLHttpRequest(flags); // 调用原生的XMLHttpRequest
monitorXHR(req); // 埋入我们的“间谍”
return req;
};

  例如将所有的请求都 post 到 test/proxy 接口,这是一个 Node 接口,代码如下。

  代码比较简单,没有考虑各种请求,例如自定义的 header、COOKIE 等。因为没有经过实践,只是展示下思路,所以肯定存在着 BUG。

  思路就是将整理好的请求地址、参数等信息转发过来后,先从数据库中查看是否有指定的 mock 数据。

  如果有就直接返回,若没有,就再去请求原接口。

router.post("/test/proxy", async (ctx) => {
const { id, method, url, params }
= ctx.request.body;
// 通过ID查找存储在 MongoDB 中的拦截记录
const row = await swww.yii666.comervices.app.getOne(id);
if (row) {
ctx.body
= row.response;
return;
}
// 没有拦截就请求原接口
const { data } = await axios[method](url, params);
ctx.body
= data;
});

  理论上,是完成了拦截,但是现在还有个很重要的问题,那就是 XMLHttpRequest 或 fetch() 那段间谍脚本该怎么注入。

2)注入脚本

  暂时想到了三个方法,第一个是通过控制 iframe 在页面中注入脚本。

  因为那张 H5 示例页面,可以放到 iframe 中呈现,所以这种注入方式理论上可行。

  只需要读取 HTMLIFrameElement 中的 contentDocument 属性就能得到页面中的 document。

document.getElementById('inner').contentDocument.body.innerHTML

  但是 iframe 有个同源限制,必须是同源的才能通过脚本读取到 contentDocu文章来源地址19351.htmlment。

  况且注入的时机也比较讲究,必须在发起请求之前,改写 XMLHttpRequest 或 fetch(),若用 Javascript 添加 script 元素,恐怕不够及时。

  那么第二个方法,就是在构建的时候将脚本注入,当然,在上线后,这些脚本都是要去除掉的,仅限测试的时候使用。

  不过这种方法不够自动化,需要研发配合,像我们这种小公司,就那么几个项目,倒也问题不大。

  第三个方法是用无头浏览器(例如 puppeteer)将脚本注入(如下所示),然后再把新的页面结构作为响应返回。

await page.evaluate(async 文章来源地址19351.html() => {
const img
= new Image();
img.src
= "xxx.png";
document.body.appendChild(img);
});
// 获取 HTML 结构
const html = await page.content();

  但有个地方要注意,输出页面结构的域名要和之前相同(需要运维配合),否则那些脚本很有可能因为跨域而无法执行了。

二、记录页面行为

  网页就是一棵 DOM 树,要记录页面行为,其实就是记录发生动作的 DOM 元素以及相关的动作参数。

  脚本注入的方式可以参考上面的 3 种方法,平台的布局也与上面的类似,只是表单中的参数可能略有不同。

1)保存 DOM 元素

  DOM 元素是不能直接 JSON 序列化的,所以需要将其映射成一个指定结构的对象,如下所示。

{
"type": "scrollTo",
"rect": {
"top": 470,
"left": 8,
"width": 359,
"height": 400
},
"scroll": {
"top": 189.5,
"left": 0
},
"tag": "div"
}

  tag 是元素类型,例如 div、button、window 等;type 是事件类型,例如点击、滚动等;rect 是坐标和尺寸,scroll 是滚动距离。

  这种结构就可以顺利存储到数据库中了。

2)监控行为

  目前实验,就只监控了点击和滚动两种行为。

  为 body 元素绑定 click 事件,采用捕获的事件传播方式。

/**
* 监控 body 内的点击行为
*/
document.body.addEventListener(
'click', (e) => {
behaviors.push({
type:
'click',
rect: offsetRect(e.target),
tag: e.target.tagName.toLowerCase()
});
},
true);

  rect 的尺寸和坐标本来是通过 getBoundingClientRect() 获取的,但是该方法参照的是视口的左上角,也就是说会随着滚动而改变坐标。

  

  所以就换了一种能更加精确获取坐标的方法,如下所示,nodeMap 是一个 Map 数据结构,key 可以是一个元素对象,用于缓存计算过的元素坐标。

// 元素缓存
const nodeMap = new Map();
/**
* 读取元素真实的坐标
*/
function offsetRect(node) {
// 从缓存中读取node信息
const exist = nodeMap.get(node);
if(exist) {
return exist;
}
let top
= 0, left = 0;
const width
= node.offsetWidth
const height
= node.offsetHeight;
while (node) {
top
+= node.offsetTop;
left
+= node.offsetLeft;
node
= node.offsetParent;
}
const rect
= { top, left, width, height };
nodeMap.set(node, rect);
// 缓存node信息
return rect;
}

  下面是对滚动的监控代码,throttle() 是一个节流函数,不节流会影响滚动的性能。

  在 startScroll() 函数中会计算滚动条距离顶部和左边的距离,window 和元素读取的属性略有不同。

/**
* 节流
*/
function throttle(fn, wait) {
let start
= 0;
return (e) => {
const now
= +new Date();
if (now - start > wait) {
fn(e);
start
= now;
}
};
}
/**
* 对滚动节流
*/
const startScroll
= throttle((e) => {
const target
= e.target;
let tag, rect, scroll;
if(target.defaultView === window) {
tag
= 'window';
scroll
= {
top: window.pageYOffset,
left: window.pageXOffset
};
}
else {
tag
= target.tagName.toLowerCase();
scroll
= {
top: target.scrollTop,
left: target.scrollLeft
};
rect
= offsetRect(target);
}
behaviors.push({
type:
'scrollTo',
rect,
scroll,
tag
});
},
100);
/**
* 监控页面的滚动行为
*/
window.addEventListener(
'scroll', (e) => {
startScroll(e);
},
true);

3)还原

  在得到数据结构后,就得让其还原,呈现完成一系列动作后的页面。

  我写的算法比较简单,还有很大的优化空间。目前就是遍历存储的行为数组,然后深度优先搜索 body 内的所有子元素。

  当坐标和尺寸满足条件时,返回元素。不过这种方式非常依赖这两个参数,因此只要结构发生变化,那么动作就无法完成。

function revert(behaviors) {
let isFind
= false;
// 深度优先遍历
const dfs = (node, target) => {
if (!node) return;
const rect
= offsetRect(node);
const tag
= node.tagName.toLowerCase();
// console.log(node, rect, target)
// 根据坐标定位元素
if (target.tag === tag &&
target.rect.top
=== rect.top &&
target.rect.left
=== rect.left &&
target.rect.width
=== rect.width &&
ta文章来源站点https://www.yii666.com/rget.rect.height
=== rect.height) {
target.node
= node; //记录元素
isFind = true;
return;
}
node.children
&& Array.from(node.children).forEach((value) => {
if (isFind) { return; }
dfs(value, target);
});
};
behaviors.forEach(item
=> {
isFind
= false;
// window对象单独处理
if(item.tag === 'window') {
item.node www.yii666.com
= window;
}
else {
dfs(document.body, item);
}
const { node }
= item;
// 没有找到符合要求的元素
if(!node) return;
switch(item.type) {
case 'scrollTo': // 滚动
node.scrollTo({
...item.scroll,
behavior:
'smooth'
});
break;
default: // 其他事件
node[item.type]();
break;
}
});
}

  scrollTo() 是一个滚动的方法,smooth 是一种平滑选项,奇怪的是,当我去掉此选项时,滚动就无法完成了。

4)截图

  本来是计划用脚本来实现截图的,可选的库是 dom-to-imagehtml2canvas

  但是测试下来得到的截图结果都不是很理想,于是就仍然采用 puppeteer 来实现截图。

  先将行为脚本注入,然后等几秒,最后再截图。这种截图得到的结果比较准确,但就是执行过程有点慢,经常需要十几秒甚至更长。

await page.evaluate(async () => {
const scrpt
= document.createElement("script");
scrpt.src
= "xx.js";
document.body.appendChild(scrpt);
});
await page.waitForTimeout(
2000);
await page.screenshot({
path: `xx
/1.png`,
type: "png"
});

  两张截图的对比可以通过 pixelmatch 完成,下面是官方提供的 node.js 使用示例,pngjs 是一个 png 图像编解码器。

const fs = require('fs');
const PNG
= require('pngjs').PNG;
const pixelmatch
= require('pixelmatch');
const img1
= PNG.sync.read(fs.readFileSync('img1.png'));
const img2
= PNG.sync.read(fs.readFileSync('img2.png'));
const {width, height}
= img1;
const diff
= new PNG({width, height});
pixelmatch(img1.data, img2.data, diff.data, width, height, {threshold:
0.1});
fs.writeFileSync(
'diff.png', PNG.sync.write(diff));

来源于:Node.js躬行记(26)——接口拦截和页面回放实验


推荐阅读
  • .NetCoreWebApi生成Swagger接口文档的使用方法
    本文介绍了使用.NetCoreWebApi生成Swagger接口文档的方法,并详细说明了Swagger的定义和功能。通过使用Swagger,可以实现接口和服务的可视化,方便测试人员进行接口测试。同时,还提供了Github链接和具体的步骤,包括创建WebApi工程、引入swagger的包、配置XML文档文件和跨域处理。通过本文,读者可以了解到如何使用Swagger生成接口文档,并加深对Swagger的理解。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 浅解XXE与Portswigger Web Sec
    XXE与PortswiggerWebSec​相关链接:​博客园​安全脉搏​FreeBuf​XML的全称为XML外部实体注入,在学习的过程中发现有回显的XXE并不多,而 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • Webmin远程命令执行漏洞复现及防护方法
    本文介绍了Webmin远程命令执行漏洞CVE-2019-15107的漏洞详情和复现方法,同时提供了防护方法。漏洞存在于Webmin的找回密码页面中,攻击者无需权限即可注入命令并执行任意系统命令。文章还提供了相关参考链接和搭建靶场的步骤。此外,还指出了参考链接中的数据包不准确的问题,并解释了漏洞触发的条件。最后,给出了防护方法以避免受到该漏洞的攻击。 ... [详细]
  • 背景应用安全领域,各类攻击长久以来都危害着互联网上的应用,在web应用安全风险中,各类注入、跨站等攻击仍然占据着较前的位置。WAF(Web应用防火墙)正是为防御和阻断这类攻击而存在 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • 网络请求模块选择——axios框架的基本使用和封装
    本文介绍了选择网络请求模块axios的原因,以及axios框架的基本使用和封装方法。包括发送并发请求的演示,全局配置的设置,创建axios实例的方法,拦截器的使用,以及如何封装和请求响应劫持等内容。 ... [详细]
  • 本文介绍了绕过WAF的XSS检测机制的方法,包括确定payload结构、测试和混淆。同时提出了一种构建XSS payload的方法,该payload与安全机制使用的正则表达式不匹配。通过清理用户输入、转义输出、使用文档对象模型(DOM)接收器和源、实施适当的跨域资源共享(CORS)策略和其他安全策略,可以有效阻止XSS漏洞。但是,WAF或自定义过滤器仍然被广泛使用来增加安全性。本文的方法可以绕过这种安全机制,构建与正则表达式不匹配的XSS payload。 ... [详细]
  • Android实战——jsoup实现网络爬虫,糗事百科项目的起步
    本文介绍了Android实战中使用jsoup实现网络爬虫的方法,以糗事百科项目为例。对于初学者来说,数据源的缺乏是做项目的最大烦恼之一。本文讲述了如何使用网络爬虫获取数据,并以糗事百科作为练手项目。同时,提到了使用jsoup需要结合前端基础知识,以及如果学过JS的话可以更轻松地使用该框架。 ... [详细]
  • 单页面应用 VS 多页面应用的区别和适用场景
    本文主要介绍了单页面应用(SPA)和多页面应用(MPA)的区别和适用场景。单页面应用只有一个主页面,所有内容都包含在主页面中,页面切换快但需要做相关的调优;多页面应用有多个独立的页面,每个页面都要加载相关资源,页面切换慢但适用于对SEO要求较高的应用。文章还提到了两者在资源加载、过渡动画、路由模式和数据传递方面的差异。 ... [详细]
  • 本文介绍了Web开发人员的输出缓冲的概念和优势,以及如何使用输出缓冲来减少下载和呈现HTML所需的时间。同时,还解决了在设置Cookie时可能遇到的问题。初学者可以通过使用输出缓冲将整个HTML页面作为一个变量来处理,从而更好地掌握Web开发。 ... [详细]
  • Spring框架《一》简介
    Spring框架《一》1.Spring概述1.1简介1.2Spring模板二、IOC容器和Bean1.IOC和DI简介2.三种通过类型获取bean3.给bean的属性赋值3.1依赖 ... [详细]
  • 【爬虫】关于企业信用信息公示系统加速乐最新反爬虫机制
    ( ̄▽ ̄)~又得半夜修仙了,作为一个爬虫小白,花了3天时间写好的程序,才跑了一个月目标网站就更新了,是有点悲催,还是要只有一天的时间重构。升级后网站的层次结构并没有太多变化,表面上 ... [详细]
author-avatar
赵晓伟
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有