大家好,我是羡辙,在百度 EFE 团队开发基于 JavaScript 的开源可视化工具 ECharts。
通常这样的技术分享,每个嘉宾都会介绍一下工作背景,但是我其实上个月初才以校招生的身份加入百度。来这里之前,百度一下,我就知道其他几位嘉宾都是业界很厉害的前辈。第一次以嘉宾的身份参加技术分享,感谢 stackoverflow——哦不,感谢 segmentfault 给予我的这次机会。啊呀,烧碱,我回去的机票钱还给报销么?
(烧碱当时表现非常夸张,一脸“你居然犯这种错误,哈哈哈哈”。其实是我设计好的段子,原来把他都骗过了哈哈!他说怪我怎么表现得这么自然~ 嘛,人生如戏,全靠演技!)
我在 2013 年 10 月的时候,在图灵社区发表了电子书《Three.js 入门指南》。 在去年百度前端学院 IFE 的课程中,我实现了一个基于 JavaScript 的 low-poly 算法,后来经过不断的尝试和改进,发表了相关论文,也成为我毕业课题的一部分。本科在英特尔实习阶段,开发了一个为网页游戏打造的轻便的音乐库,包装了 Web Audio。
此外,我还基于兴趣做过不少有意思的小项目,因为跟各位前端大神相比,实在班门弄斧,所以在这里就不一一介绍了,有兴趣的话可以到我的 github 看一下。
首先,我想做个小调查,在座各位没有听说过 ECharts 的请举一下手。
(现场三百多人,两三个人举手)
啊呀,在你们这群从小不爱举手的人面前,我好想再调研一下没有听说过羡辙的。不过,机智如我,这种打脸的事是不会做的。
我今天也不打算多安利 ECharts,毕竟名声在外。如果是不了解的朋友,对于今天的分享主题,你需要知道的也就是 ECharts 是最好用的前端可视化工具——咱谦虚点,加个“之一”吧!
作为一个第三方库,测试的重要性不言而喻。
而在测试渲染相关的产品时,我们首要的目标就是保证渲染结果的正确性。在此基础上,如果能够尽可能自动化地进行测试,就可以更有效率、甚至更可靠地发现问题。
ECharts 是基于 Canvas 实现的。对 Canvas 的测试,直觉上我们可以通过比较像素的一致来判断,但如何描述测试用例的预期表现、如何保证版本间渲染效果的一致性、如何快速定位到 bug 等等问题,对于前端可视化实践而言,仍然是一个很新的话题。
因此,今天我想和大家分享一些在我们为 ECharts 做测试时候获得的经验,希望能和大家针对“如何测试前端可视化产品”这一主题展开讨论。
在此之前,让我们先来了解一下,比较通用的前端测试在测些什么。
可能对前端了解并不多的人,会认为前端等同于用户界面 UI。但随着项目业务逻辑越来越复杂,前端的测试和 native 的、PC 的测试类型并没什么差别。
从测试的指标来分,可以分为功能测试和非功能测试,后者主要是指性能。
用户界面通常是比较难测的一种,而之所以困难,很多时候就是因为预期的难以描述。
提到前端的测试框架,可能最有名的就是这三个了。
最老牌的 QUnit 发布于八年前,仰仗 jQuery 的大腿名噪一时。
Jasmine 的发布使大家突然开始关注行为驱动开发(BDD)。
我想,大家应该都知道 TDD,Test Driven Development,测试驱动开发。TDD 一个很重要的思想是“测试先行”,也就是先针对接口先写测试用例,mock 数据使得测试用例都通过,然后再实现接口。在这一过程中,如果引入了非预期的测试结果,第一时间就能发现,而不是在瀑布开发模型中,等整个项目都完成了再测试,这时候的软件可能已经是千疮百孔,很难定位到 bug。
而行为驱动开发 BDD 其实和 TDD 并不矛盾,或者说,它也是提倡测试先行的。其实这两者的差异非常微妙,更多表现在测试语法的差异上,这一点我们稍后再了解。
Mocha 是在 Jasmine 推出后两年,也就是 2012 年发布的。因为同样是 BDD 的风格,所以和 Jasmine 的相似度非常高,只是支持更多的插件,因此可配置性更高。你可以使用自己喜欢的断言库配合 Mocha,比如这个叫 Chai 的断言库就比较有名。这个看似中文拼音的名字,其实是取 chain 链条这个英文单词的开头部分,因为该断言库支持链式语法,这也是 BDD 的一大风格。
我们可以看到,Jasmine 的语法和 Mocha 是非常类似的,而 jUnit 显示是另一种风格。从这里我们其实就能比较清楚地看到 TDD 和 BDD 的差异。
从语法的角度,我们发现 TDD 的语法还是不够直观,可能不熟悉的人还要去查这个 test 和 equal 函数对应的参数是什么。相比之下,BDD 的语法非常接近自然语言。比如我们描述一个测试用例,它应该怎么怎么样,我们期望什么是什么等等。比较 Jasmine 和 Mocha 的语法,我们会觉得有非常多的共同点,比较明显的差异就是 Mocha 在开头 require 了 Chai 这个断言库。一般认为 Mocha 对于插件的选择更为灵活,而 Jasmine 因为不需要引入其他插件,所以对于没有特殊要求的情况下,稍微方便一些。
通常我们说的用户界面相关的测试,通常是指这些——
- 按钮在屏幕的显示位置是否正确?
- 按钮在某个时刻是否是禁止状态?
- 在某个点击事件后,浏览器的标题是否符合预期?
- ……
那么,如何自动化测试这些场景呢?上面说的这些通用的前端测试框架就不适用了。
Nightwatch 是模拟浏览器行为的框架的代表。
从这段代码我们可以看到,这个测试框架可以设定访问的 URL 地址,然后等待一秒,直到 body 出现。然后,断言标题为 Google 并且搜索框可见。接着,将搜索框内容设为某个值,等待一秒直到搜索按钮出现,点击搜索,等一秒,断言搜索到的第一条是 Wikipedia 的记录。
以上是比较典型的前端测试,但是我们今天重点要讨论的是测试和渲染相关的部分。与用户界面无关的测试,其实已经讨论地比较充分。但是与界面相关的测试,非但前端做得比较少,就是比较传统的客户端 UI 测试,一直也是做得不充分的,要么依赖人眼看,要么索性不做,因为测试界面相关的成本是很高的。而说到前端渲染相关的测试,往往则更加测得少。但是 ECharts 作为一个可视化产品的第三方库,测试渲染相关部分又是非常重要的。
我们知道 HTML、SVG 的测试可以借鉴前面 Nightwatch 的例子,主要是通过 DOM 树去分析是否符合预期。但 ECharts 主要是基于 Canvas 的,那 Canvas 怎么测?
直观上,我们觉得有这么两种方法,一个是人眼看,还有一个是使用 toDataURL 保存成图片比较。但是在实际操作的过程中,它们都是有比较大问题的。
比如靠人眼看,我们知道,ECharts 的配置项非常非常多,因此对应的测试用例也多到难以想象。而且我们平均一两周就发布一个新版本,如果每次新版本发布前的测试,都要依赖人眼看,那这个工作量是非常巨大的。另一方面,人眼观察的结果肯定并不精确,对于细小的渲染差异,可能无法发觉。
同样,通过比较 Canvas 图像的方法,图像相同就意味着渲染正确了吗?我们应该如何设计用例来描述预期,并且尽可能自动化地进行测试?这些都是我们今天想和大家具体探讨的。
首先,我们来看一下 ECharts 的渲染无关部分是怎么测试的。
比如这个线性数据管理模块 List
,我们使用 Jasmine 做单元测试。这个 utHelper.prepare
是我的自己写的一个辅助函数,主要用于清理测试环境,并且加载需要测试的库。然后就是测试 list 的内容是否符合预期,还是 Jasmine 的套路,没什么新的内容,我们很快的过一下。那么,如何测渲染相关部分呢?
Echarts 的大部分设置都是通过一个叫 setOption
的函数传入配置项的。这里我们要测试 titile.text
配置项能否和文档所说,可以使用 \n
换行。
我们来看手动测试应该怎么做。我们设置好参数,然后查看是否渲染的结果在预期的位置换行了。
结果是这样的,然后我们认为这个用例表现正常,符合预期。
那么,如果要自动化做这样的测试呢?
有这么几种思路。
首先我们可以给定两组不同的配置项,我们期望这两个配置项得到的渲染结果相同或者不同。
期望相同的例子比如,一个配置项不设置字体,另外一个配置项设置等于文档中所说的默认字体颜色,然后,我们期望这两者得到的渲染结果相同。期望不同的例子比如,一个配置项不设置字体颜色,另外一个设置了一个不等于默认的颜色,然后,我们期望这两者得到的渲染结果不同。
此外,为了确保渲染的正确性,并且尽可能自动化,我们只对某个特定版本使用人眼判断,作为基线版本,在此之后,每个版本都和这个人眼看过的版本的 Canvas 进行比较。
当然,如果 Canvas 和基线版本的不一样,就一定能说这是个 bug 吗?很有可能就是这个版本我们实现了一个新功能,导致渲染的结果本该不同。那么,在这种情况下,如何快速定位到这个问题,并且判断出这是个 bug 还是 feature 呢?
首先,我们可以使用 js-imagediff 生成两个 Canvas 的 diff 图像,然后就能很清楚地看到差异处。其次,我们可以比较对 Canvas 的操作栈,也就是所有 Canvas 操作,如在某处使用某种颜色绘制了特定大小的长方形。
那么,Canvas 要怎样才算相等呢?
很容易想到的一个办法是使用 Canvas.toDataURL
生成 Canvas 的图像进行比较,如果字符串相同,那么认为图像相同。
此外,Canteen 这个库帮我们记录对 Canvas 的操作,提供比较上述的操作,也能比较 Canvas 的差异。
我们知道,对于同一个底层绘图环境,Canvas 操作相同,渲染出的图像一定相同,反之则不成立。
所以我们说,操作一致是图像一致的充分非必要条件。
(说到这里,大家都笑了,然而这本来并不是个段子……大概是我非常严肃地说了这么句学究气这么重的句子……)
实践发现,比较 Canvas 内容的操作更简单,因为它不依赖第三方库。而相比之下,比较 Canvas 操作是一种更严格的测试,因此也更容易帮我们发现潜在错误。
之前说的 Canvas diff 的效果是这样的。我们乍一看,左边和中间的两个图非常相似,甚至对于接近度更高的图像,可能处女座都不一定看得出区别。在这种情况下,我们看一下 diff 图,只要图中有不是黑色的部分,我们就知道这里出现差别了。所以,这是一种能帮我们快速定位到问题所在的方法。
接下来,让我们通过具体的例子来说明。
我们想要测试默认的字重是不是等于 normal。为此,我们写了两个配置项,其中一个的字重没设置,另外一个设为 normal,我们期望这两者得到的渲染效果一致。
比较 Canvas 的内容和操作我们发现,内容是一致的,但操作是不一致的。
分析操作栈发现,在 Canvas 上绘制时使用了 bolder 操作,但是由于该字体家族不存在粗体字重,因此实际的显示效果和 normal 相同。因此,这其实是一个 bug。
所以我们说,在这个测试用例中,比较 Canvas 操作得到的结论是正确的,比较 Canvas 内容得到的结论是漏报的。
我们再来看另外一个例子。
这里,我们要测试标题样式设为 oblique
时,是否被正确地渲染。我们同样写了两个配置项,其中一个设为 oblique
,另外一个设为 italic
。我们期望他们渲染的结果不同。
首先,我们来分析这个案例存在的一个潜在问题。我们希望测试标题样式为 oblique
,但实际上我们只能通过判断该样式与 normal
、italic
等的结果不同,从侧面得出结论。这一点在软件测试中是一个普遍存在的潜在问题。我们只能从第一次用人眼看,后面跟这次比较来缓解。
从实际的渲染结果看,Canvas 内容是一致的,而操作是不一致的。这是为什么呢?
大家应该都见过 CSS font-style
属性有 oblique
和 italic
属性,都表示斜体,那么有人知道这两者有什么区别吗?
(没人举手)
我也是写了这个测试用例失败了,去搜了才知道的。其实 italic
是字体设计师手动绘制的斜体字,而 oblique
是计算机使用斜切处理,把 normal
的字体处理成斜体的。而一般字体厂商都不会同时提供 oblique
和 italic
样式,通常是其中一个不存在的时候,就使用另外一种替代。
因此,在这个案例中,倒也不能怪比较内容得到的结论是误报的,应该说这是写测试用例的人不知道这点造成的。但这也不能怪我啊,你看你们也都不知道 oblique
和 italic
的差别嘛!:D
所以说,比较操作更容易让我们发现问题。
我们的结论是,通常比较 Canvas 操作得到的结论更稳健。但比较操作也不总是符合预期的,取决于测试用例,大家可以自己思考一下反例。
我们推荐的比较方式是,先比较 Canvas 操作;如果测试失败,查看 Canvas 图像 diff 和操作栈 diff。
总结一下,可视化相关测试的思路是,对渲染无关部分做单元测试,渲染相关部分做 UI 测试。可以测试不同配置项设置下的渲染一致性、测试不同版本的渲染一致性,在遇到错误时,可以查看 Canvas 图像与操作栈 diff 分析失败原因。
扩展阅读
-
Which JavaScript Test Library Should You Use? QUnit vs Jasmine vs Mocha
-
Comparison of three major javascript unit testing frameworks
最后,感谢大家对 ECharts 的支持,是大家的使用才使得我们能够越做越好~