前端可视化的测试实践

SegmentFault D-Day 厦门站分享

百度前端工程师
2016.05.28

羡辙

个人经历

2016.4 至今:开源图表库 ECharts @百度

2009-2016:本科+硕士 @上海交通大学 软件学院,数字艺术媒体实验室

技术作品

更多作品参见 github.com/Ovilia

ECharts

  • 一个纯 Javascript 的图表库
  • 兼容主流浏览器,移动友好
  • 底层依赖轻量级的 Canvas 类库 ZRender
  • 提供直观、生动、可交互,可高度个性化定制的数据可视化图表

测试渲染相关部分

目标

  • 像素级精确测试渲染结果
  • 尽可能自动化测试

问题

  • 如何描述测试用例的预期表现?
  • 如何保证版本间渲染效果的一致性?
  • 如何快速定位到 bug?
  • 如何测试不同设备的表现?
  • ……
如何测试前端可视化产品?

当我们谈论前端测试的时候,
我们在谈论什么

功能测试

  • 逻辑
  • 用户界面
  • 兼容性
  • ……

性能测试

  • 时间
  • 空间
  • ……

主流前端测试框架

QUnit

  • 老牌的单元测试框架
  • jQuery 家族钦定测试框架

Jasmine

  • 行为驱动Behavior-Driven)的测试框架
  • 支持异步测试

TDD vs. BDD

  • TDD(Test-Driven Development)测试驱动开发
  • BDD(Behavior-Driven Development)行为驱动开发

Mocha

  • BDD,语法和 Jasmine 相似度极高
  • 支持更多插件,如断言Assertion)库 Chai

大同小异的语法

// QUnit
test("pow(2, 2) should return 4", function(){
    equal(math.pow(2, 2), 4, "result was " + result);
});
test("pow(2, 3) should return 8", function(){
    equal(math.pow(2, 3), 8, "result was " + result);
});

// Jasmine
describe("pow", function(){
    it("should raise 2 to the power of 2", function(){
        expect(math.pow(2, 2)).toBe(4);
    });
    it("should raise 2 to the power of 3", function(){
        expect(math.pow(2, 3)).toBe(8);
    });
});

// Mocha
var expect = require('chai').expect;
describe("pow", function(){
    it("should raise 2 to the power of 2", function(){
        expect(math.pow(2, 2)).to.equal(4);
    });
    it("should raise 2 to the power of 3", function(){
        expect(math.pow(2, 3)).to.equal(8);
    });
});

用户界面相关测试

  • 按钮在屏幕的显示位置是否正确?
  • 按钮在某个时刻是否是禁止状态?
  • 在某个点击事件后,浏览器的标题是否符合预期?
  • ……

如何自动化测试这些场景?

Nightwatch.js

自动化测试浏览器相关操作

module.exports = {
  'Demo test Google' : function (client) {
    client
      .url('http://www.google.com')
      .waitForElementVisible('body', 1000)
      .assert.title('Google')
      .assert.visible('input[type=text]')
      .setValue('input[type=text]', 'rembrandt van rijn')
      .waitForElementVisible('button[name=btnG]', 1000)
      .click('button[name=btnG]')
      .pause(1000)
      .assert.containsText('ol#rso li:first-child',
        'Rembrandt - Wikipedia')
      .end();
  }
};

如何自动化测试渲染相关部分?

HTML、SVG

可使用 Nightwatch.js 这类库测试 DOM 结构

Canvas

  • 渲染出结果,靠人眼看
    • 重复工作量大
    • 人眼无法识别细微差别
  • toDataURL() 保存成图片
    • 比较图片相同?
    • 如何描述期望?
    • 如何尽可能自动化测试?

对 ECharts 做测试

渲染无关部分

Jasmine 单元测试

例:线性数据管理模块 List

describe('List', function () {
    var testCase = window.utHelper.prepare(['echarts/data/List']);

    describe('Data Manipulation', function () {
        testCase('initData 1d', function (List) {
            var list = new List(['x', 'y']);
            list.initData([10, 20, 30]);
            expect(list.get('x', 0)).toEqual(10);
            expect(list.get('x', 1)).toEqual(20);
            expect(list.get('x', 2)).toEqual(30);
            expect(list.get('y', 1)).toEqual(20);
        });

        // ...
    });
});

如何测试渲染相关部分?

渲染相关部分

例:配置项 title.text 允许 \n 表示换行

手动测试

  • 设置参数
  • 人眼查看渲染效果是否换行
chart.setOption({
    series: [],
    title: {
        text: 'first line\nsecond line'
    }
});

半自动测试

如何测试?

  • 测试不同配置项设置下的渲染一致性
    • 期望相同:如设置等于默认值的字体颜色,期望与默认情况相同
    • 期望不同:如改变字体颜色,期望与默认情况不同
  • 测试不同版本的渲染一致性
    • 以某次发布版本为基线,手动查看效果
    • 以后每次发布比较和基线比较 Canvas 是否一致
  • 对于失败的案例
    • 使用 js-imagediff 输出 Canvas 图像的 diff 图
    • 比较 Canvas 操作栈

如何比较 Canvas?

  • 使用 canvas.toDataURL() 比较 Canvas 图像是否一致
  • 使用 Canteen 比较 Canvas 操作是否一致
对于同一个底层绘图环境,Canvas 操作相同,渲染出的图像一定相同,反之则不成立

操作一致是图像一致的充分非必要条件

比较 Canvas 内容

操作更简单,不依赖第三方库

比较 Canvas 操作

更严格的测试,发现潜在错误

Canvas Diff

Canvas 1 Canvas 2 Canvas Diff

测试用例 (1)

测试标题字重默认值是 normal

var testCase = {
  name: 'should display bold font weight',
  option1: {
    series: [],
    title: {
      text: 'bold font vs. normal font',
      textStyle: {
      }
    }
  },
  option2: {
    series: [],
    title: {
      text: 'bold font vs. normal font',
      textStyle: {
        fontStyle: 'normal'
      }
    }
  }
};

var optionCompare = function(isExpectEqual, title, option1, option2) {
  it(title, function(done) {
    require(['newEcharts'], function (ec) {
      var canvas1 = helper.getRenderedCanvas(ec, option1);
      var canvas2 = helper.getRenderedCanvas(ec, option2);

      // canvas context and images
      var ctx1 = canvas1.getContext('2d');
      var ctx2 = canvas2.getContext('2d');
      var img1 = canvas1.toDataURL();
      var img2 = canvas2.toDataURL();

      // compare canvas content or operation stack
      var compare1 = compare2 = null;
      if (STRATEGY === 'content') {
        compare1 = img1;
        compare2 = img2;
      } else if (STRATEGY === 'stack') {
        compare1 = ctx1.hash();
        compare2 = ctx2.hash();
      }

      // expect to equal, or not
      if (isExpectEqual) {
        expect(compare1).toEqual(compare2);
      } else {
        expect(compare1).not.toEqual(compare2);
      }

      done();
    });
  });
};

optionCompare(true, testCase.name, testCase.option1, testCase.option2);

用例分析 (1)

比较 Canvas 内容 vs. 操作

测试结果表明:两种配置的 Canvas 内容是一致的,而操作是不一致的。

分析操作栈发现,在 Canvas 上绘制时使用了 bolder 操作,但是由于该字体家族不存在粗体字重,因此实际的显示效果和 normal 相同。

因此,这是一个 bug。

在本案例中,比较 Canvas 操作得到的结论是正确的,比较 Canvas 内容得到的结论是漏报false negative)的。

测试用例 (2)

测试标题样式设为 oblique 时,没有使用 italic[1]

var testCase = {
    name: 'should display oblique different from italic',
    option1: {
        series: [],
        title: {
            text: 'oblique vs. italic',
            textStyle: {
                fontStyle: 'oblique'
            }
        }
    },
    option2: {
        series: [],
        title: {
            text: 'oblique vs. italic',
            textStyle: {
                fontStyle: 'italic'
            }
        }
    }
};

[1] italic 表示由设计师手动绘制的斜体字,而 oblique 是在显示时,在原字体样式上做斜切处理。事实上,很少有字体同时存在这两种样式,通常都是互相通用的。参见 font-style: italic vs oblique in CSS

用例分析 (2)

该案例存在的潜在问题

如果我们希望测试标题样式为 oblique 时,是否真正设置正确,我们是无从很难判断的。

我们只能判断该样式与 normalitalic 等的结果不同,从侧面得出结论。

这一点在软件测试中是一个普遍存在的潜在问题。

比较 Canvas 内容 vs. 操作

测试结果表明:两种配置的 Canvas 内容是一致的,而操作是不一致的。

分析操作栈发现,绘制时的确分别使用了 obliqueitalic,但是由于两者显示效果一致,所以产生了以上分歧。

因此,这不是一个 bug。

在本案例中,比较 Canvas 操作得到的结论是正确的,比较 Canvas 内容得到的结论是误报false positive)的。

结论

通常比较 Canvas 操作得到的结论更稳健

比较操作也不总是符合预期的,取决于测试用例

(思考反例)

推荐的比较方式

  • 先比较 Canvas 操作
  • 如果测试失败
    • 查看 Canvas 图像 diff
    • 查看 Canvas 操作栈 diff

前端可视化的测试实践

可视化相关测试的思路

  • 渲染无关部分做单元测试
  • 渲染相关部分做 UI 测试
    • 测试不同配置项设置下的渲染一致性
    • 测试不同版本的渲染一致性
    • 查看 Canvas 图像与操作栈 diff 分析失败原因

扩展阅读

羡辙,再见

微信公众号:羡辙部落格(xianzheblog)

PPT 访问地址:zhangwenli.com/ppt

GitHub | dribbble | Instagram | 微博 | 豆瓣 | 知乎 | 个人网站

调研问卷