大家好,我是羡辙,在百度 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,但实际上我们只能通过判断该样式与 normalitalic 等的结果不同,从侧面得出结论。这一点在软件测试中是一个普遍存在的潜在问题。我们只能从第一次用人眼看,后面跟这次比较来缓解。

从实际的渲染结果看,Canvas 内容是一致的,而操作是不一致的。这是为什么呢?

大家应该都见过 CSS font-style 属性有 obliqueitalic 属性,都表示斜体,那么有人知道这两者有什么区别吗?

(没人举手)

我也是写了这个测试用例失败了,去搜了才知道的。其实 italic 是字体设计师手动绘制的斜体字,而 oblique 是计算机使用斜切处理,把 normal 的字体处理成斜体的。而一般字体厂商都不会同时提供 obliqueitalic 样式,通常是其中一个不存在的时候,就使用另外一种替代。

因此,在这个案例中,倒也不能怪比较内容得到的结论是误报的,应该说这是写测试用例的人不知道这点造成的。但这也不能怪我啊,你看你们也都不知道 obliqueitalic 的差别嘛!:D

所以说,比较操作更容易让我们发现问题。

我们的结论是,通常比较 Canvas 操作得到的结论更稳健。但比较操作也不总是符合预期的,取决于测试用例,大家可以自己思考一下反例。

我们推荐的比较方式是,先比较 Canvas 操作;如果测试失败,查看 Canvas 图像 diff 和操作栈 diff。

总结一下,可视化相关测试的思路是,对渲染无关部分做单元测试,渲染相关部分做 UI 测试。可以测试不同配置项设置下的渲染一致性、测试不同版本的渲染一致性,在遇到错误时,可以查看 Canvas 图像与操作栈 diff 分析失败原因。

扩展阅读

最后,感谢大家对 ECharts 的支持,是大家的使用才使得我们能够越做越好~