weixin_39717110
weixin_39717110
2021-01-01 04:43

测试工具设计的一个细节:iframe 还是 window ?

本文讨论的是基于 socket 的测试方案,应该在 iframe 还是在 window 中运行测试的问题。

索引

  • 基于 socket 的测试方案
  • iframe vs window
  • 测试效率
  • 浏览器稳定性
  • 断言正确性
  • 打开新窗口运行?没那么容易!
  • 不可忽视的窗口名
  • 窗口间的数据传递
  • 允许弹出窗口设置
  • 移动端?

一、基于 socket 的测试方案

先用一张图简单说明此类方案的工作原理:

totoro-structure

这是一个典型的 CS 结构,但除了 Client 和 Server 之外,还有一个特殊的角色:Labor,即用于运行测试的浏览器。大体的工作步骤如下: 1. Server 启动测试服务。 2. 任意浏览器通过访问 Server 的地址注册成为 Labor。 3. Client 向 Server 发起测试请求。 4. Server 将测试任务分配给相应的 Labor 去执行。 5. Labor 们将各自的测试结果返回给 Server。 6. 所有 Labor 完成测试后,由 Server 将汇总的测试结果发送给 Client。 7. Client 生成测试报告。

有兴趣的同学可以访问 https://github.com/totorojs/totoro-server/tree/master/docs/totoro 的 keynote 了解更多。

那么 Labor 到底是怎么运行测试的?

让我们稍微多花一点时间来关注这个细节。

下图为一个空闲状态的 Labor,它通过 socket 跟 Server 进行通讯,等待测试任务的分配。

labor

某个时刻,Server 给该 Labor 分配了两个测试任务。

如果测试在 iframe 中运行,则如下图所示(两个浅蓝色的方框即正在运行测试的 iframe ):

labor-run-order-in-iframe

当然,也可以在 window 中运行(打开了两个新窗口):

labor-run-order-in-window

我们将运行测试的 iframe 或 window 称为 Runner。

不管是通过哪种方式运行测试,Runner 都会把测试报告发送给打开它的 Labor,由 Labor 根据一定的时间间隔统一发回给 Server。

那么这两种运行方案到底哪种更好呢?还是其实差不多?

二、iframe vs window

基本上,所有基于 socket 的测试方案都选择在 iframe 中运行测试,为啥?简单呀,起码不用操心弹出窗口被屏蔽的问题。所以 totoro 最初自然也选择了使用 iframe。

在使用 iframe 方案的同时,也会想: “window 方案会不会是更好的选择?”,首先想到的是效率问题。

测试效率

一般来说,一个浏览器窗口或者 tab 是独立进程,iframe 则和父窗口共享进程。所以直觉上会认为,在 window 中运行测试会比较快。为了证明这个猜想,很快写了一个 demo,在多个浏览器中分别使用 iframe 和 window 方案多次运行同一个测试,以下为 IE6 的两个代表截图: 1. 某次使用 iframe 运行测试的结果

run in iframe 2. 某次使用 window 运行测试的结果

run in window

可以看出:这两种方式的测试运行效率差不多。于是,当时就觉得大家都用 iframe 方案果然是有道理的。

可随着 totoro 使用的深入,我们发现:这两种方案对浏览器的稳定性影响是不一样的。

浏览器稳定性

totoro 允许数个测试并行执行,这无疑增加了浏览器的负担。使用 iframe 方案时,能明显观察到 IE6~8 更容易崩溃,window 方案的表现就好些(未深究,新进程的原因?)

当然,这个问题并没有促使 totoro 转换到 window 方案,反正无论使用哪个方案都会发生浏览器崩溃,只是频率上的差别而已,我们可以通过浏览器守护程序来及时重启崩溃的浏览器,直到频繁遇到下面的问题。

断言正确性

一般来说,脚本在 iframe 中运行,或是在 window 中运行,结果应该都是一样的,但两者又有一些微妙的差异,主要表现在对 DOM 的处理上。我们来看下面一段测试代码:


it('width is not zero', function(){
    var el = document.createElement('div')
    document.body.appendChild(el)
    expect(el.offsetWidth).not.to.be(0)
})

这个断言显然是应该成功的,但使用 iframe 方案时,却失败了:脚本认为新建的空 div 宽度为 0。

iframe-failure

这仅仅是我们遇到的诸多问题中的一个,幸运的是,这个问题可以通过放大 iframe 的尺寸来解决,只是尺寸多少合适?谁都说不准,得一个个浏览器测,更不是一劳永逸,所以 Labor 打开 iframe 的尺寸先后从 0x0,到 60x60,120x120,再到 100%x120。可是如果要测试的代码稍微复杂一些,比如本身还包含对 iframe 的操作,基本上就无法通过测试了。

由于不堪忍受 “为什么我的测试在自己的电脑上能跑过,用 totoro 却会报错?” 这种问题的频繁滋扰,终于决定切换到 window 方案。

三、打开新窗口运行?没那么容易!

不可忽视的窗口名

打开新窗口的代码十分简单:


window.open(URL,name,specs,replace)

最初 totoro 仅指定了第一个参数,而 Labor 可能同时打开多个新窗口,代码类似于:


window.open(url1)
window.open(url2)

结果发现,某些高级浏览器能正常打开两个新窗口,分别访问 url1 和 url2,可是,又是 IE 不争气啊(包括但不限于 IE6/7),例如: - IE6 只能打开一个访问 url1 的新窗口 - IE7 会先打开一个访问 url1 的新窗口,很快这个窗口又跳转到 url2

原因不深究,没什么意义。直接上解决方案:为每个新窗口指定一个不同的 name。即:


window.open(url, id)

窗口间的数据传递

回顾一下本文最初对于 Labor 工作细节的介绍:“不管是通过哪种方式运行测试,Runner 都会把测试报告发送给打开它的 Labor,由 Labor 根据一定的时间间隔统一发回给 Server。” 伪代码如下:

Labor 中的代码:


cache = []

totoro = {
    report: function(data) {
        cache.push(data)
        closeRunner()
    }
}

setInterval(function() {  //每隔一秒向 Server 发送一次报告,并清空 cache
   socket.emit('report', cache)
   cache = []
}, 1000)

Runner 测试结束时向 Labor 发送测试报告的代码:


window.opener.totoro.report(JSONData)  //调用 Labor 的 report 方法,把报告存放到 Labor 的 cache 中。

代码非常简单,不解释。

当 Labor 向 Server 发送测试报告时, IE<= 10 会出现以下报错 (或者不报错,但也不能正常工作):

被呼叫方(服务器 [不是服务器应用程序])不可用并已消失。所有连接均无效。没有执行呼叫。

The callee (server [not server application]) is not available and disappeared; all connections are invalid. The call did not execute.

经排查后发现引发错误的3个条件: 1. 子窗口将复杂格式的数据传递给主窗口 2. 子窗口关闭 3. 主窗口试图再次访问来自于子窗口的数据,比如使用 socket 发送数据(网上有资料说 ajax 也会引发这个错误)

从上面的伪代码可以看出,当测试结束时 Runner 给 Labor 发送完测试报告就立即关闭了,而一段时间后 Labor 向 Server 发送报告时还会访问来自 Runner 的数据。这完全符合上述引发错误的三个条件。

找到问题的原因,解决方案就明了了(二选一): - 测试结束后,停顿较长时间再关闭 Runner。 - 深拷贝 Runner 发送到 Labor 的数据。

一来是停顿多长时间比较合适(setInterval 可不是那么精准的)?此外还考虑到其他一些因素,最终选择深拷贝方案。

跨窗口的数据深拷贝

深拷贝代码一定会涉及类型判断,问题是 “如何进行类型判断?”

看到这你可能会觉得好笑,类型判断这种玩意,在各种类库中不是信手拈来么?目前主流的是使用 toString() 方法的一个变种 (有兴趣的同学可以查看 seajs/src/util-lang.js 获得完整代码),基本原理是:


{}.toString.call([]) -> [object Array]
{}.toString.call({}) -> [object Object]
{}.toString.call(fn) -> [object Function]

使用这个方法依次处理跨窗口数据时,我们得到的结果是:


safari, chrome, IE9/11  
    [object Undefined]
    [object Undefined]
    [object Undefined]

IE6/7/8
    [object Object]
    [object Object]
    [object Object]

firefox, IE10
    [object Array]
    [object Object]
    [object Function]

此外,还有 toString(), typeof, instanceof 也可以被用来做类型判定。完整的测试代码见 totoro-server/docs/type-checking

简而言之,这个问题是由跨 context 引起的,即两个窗口的全局对象 window 不一样,可参见 Cross-context isArray and Internet Explorer

鉴于上述原因,我们显然不能通过常规的手段来进行类型判定,而是采取了曲线救国的方式=。= 比如:


function isArray(obj) {
    return ((typeof obj.length !== 'undefined') &&
        (typeof obj.splice !== 'undefined'))
}

允许弹出窗口设置

这是切换到 window 方案必须要解决的问题,体力活,参见 totoro 的 wiki: 弹出窗口设置

移动端?

至于移动端,仍然使用 iframe 运行测试。因为 window.open 在移动设备上基本就是废的!

结束语

这是一个看似很小的问题,解决的过程可真折磨人。好在达到了预期的效果,之前在 iframe 方案中出现的断言错误已经全部都通过了。

该提问来源于开源项目:fool2fish/blog

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享
  • 邀请回答

18条回答

  • weixin_39891438 weixin_39891438 4月前

    看目录就很清晰的说,先赞再看

    点赞 评论 复制链接分享
  • weixin_39595430 weixin_39595430 4月前

    顶 大赞沉姐姐

    点赞 评论 复制链接分享
  • weixin_39736150 weixin_39736150 4月前

    那个停顿多长时间的结果是啥?

    点赞 评论 复制链接分享
  • weixin_39632891 weixin_39632891 4月前

    非常专业的文章,问题、解决思路都整理的很仔细,赞极致精神。

    点赞 评论 复制链接分享
  • weixin_39717110 weixin_39717110 4月前

    理论上来说,只要停顿的时间超过 Labor 发送消息的时间间隔就是安全的(虽然 setInterval 不一定那么准确),我们可以把这个停顿设大一点嘛。

    但是这里没有选择这个方案还有其他原因: 1. 防止已生成的数据被篡改。有的测试有可能在测试完成后产生错误(当然这是非正常状况),这个时候已经产生的测试结果如果还没被发出去就会被修改,导致测试结果不准确。 2. 在 totoro 新增的调试功能中,需要对调试数据进行过滤,比如将 DOM 对象转换成普通字符串

    点赞 评论 复制链接分享
  • weixin_39713686 weixin_39713686 4月前

    文档读起来很顺畅,nice!

    点赞 评论 复制链接分享
  • weixin_39814482 weixin_39814482 4月前

    相当赞啊

    点赞 评论 复制链接分享
  • weixin_39574246 weixin_39574246 4月前

    写的非常好呀!赞

    有一个小疑惑,“使用这个方法依次处理跨窗口数据时”产生类型判断失效的原因是context不一致,在这样数据处理的时候,是否可以参考“序列化”的方式,把需要传递的数据转化成字符串,在opener里面用“反序列化”来进行解析?

    点赞 评论 复制链接分享
  • weixin_39717110 weixin_39717110 4月前

    序列化也是一个手段,但是因为要序列化和反序列化,消耗相对高啊

    点赞 评论 复制链接分享
  • weixin_39956558 weixin_39956558 4月前

    鱼姐牛逼

    点赞 评论 复制链接分享
  • weixin_39574246 weixin_39574246 4月前

    我想了一下,对于可以不使用Labor做上下文的纯数据做序列化和反序列化的消耗还是有价值的:) 因为序列化之后的数据可以逃逸出Labor的上下文环境,还原的时候当成本地数据来处理,可以避免跨frame的兼容性问题。

    点赞 评论 复制链接分享
  • weixin_39717110 weixin_39717110 4月前

    拖了好久才来仔细思考序列化的这个想法。

    序列化的本质就是根据某种规则进行编码和解码,JSON 就是非常流行的序列化格式之一。刚好我们用以传输数据的 socket.io 模块就是使用 JSON.stringify 和 JSON.parse 来进行序列化和反序列化。

    序列化在不同的环境中有不同的限制,根据 JSON 规则 ,function 之类的复杂对象是不能被序列化的(默默地过滤掉了),而如果尝试对 DOM 进行序列化的时候,还会报循环引用的错误。(扯远点,在两端环境对等的情况下,注意是环境对等!,序列化也通常只序列化对象属性,而不序列化对象方法,在另一端会根据这个序列化的内容还原一个包含完整属性和方法的对象)

    所以,无论是深拷贝,还是序列化,都需要对 function 或者 dom 之类的对象做特殊处理。比如 totoro 的深拷贝会将这两个对象简单转换成字符串 "function" 和 "dom:tagname" 输出。

    这么看来,不赞同使用序列化的理由有二: 1. 无论是深拷贝还是序列化,在浏览器端都会又一次消耗,但序列化在数据到达服务端后还有一次反序列化的消耗。 2. 在浏览器窗口之间,无论使用深拷贝还是序列化都能解决 上下文环境 带来的问题。而浏览器端的 js 运行环境和服务端的 node 环境是完全不一样的,数据都没办法 100% 复原的,实际上连传输给服务端的机会都没有,因为 function 和 dom 都不能被序列化。

    点赞 评论 复制链接分享
  • weixin_39797264 weixin_39797264 4月前

    看不懂

    点赞 评论 复制链接分享
  • weixin_39519072 weixin_39519072 4月前

    赞,相当赞。深入浅出,文档典范。

    点赞 评论 复制链接分享
  • weixin_39594296 weixin_39594296 4月前

    话说…… 我都没想这些,因为是集成内核的,没这些问题干扰……反之,带来的缺陷也很严重的说。

    点赞 评论 复制链接分享
  • weixin_40003767 weixin_40003767 4月前

    赞!

    点赞 评论 复制链接分享
  • weixin_39622123 weixin_39622123 4月前

    关注沉鱼mm很久了

    点赞 评论 复制链接分享
  • weixin_39535287 weixin_39535287 4月前

    其实我一直有个疑问,支付宝有人的花名叫 落雁 吗?

    点赞 评论 复制链接分享

相关推荐