如何编写Canvas的单元测试

2021-07-09 by uino 29 研发

关于Canvas

Canvas是H5推出的更高性能的画布,与传统的SVG不同,它性能更高,并且是无对象模式

Canvas的例子

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Canvas的单元测试</title>
  </head>
  <body>
    <canvas width="300" height="200" style="border: 1px solid #000"></canvas>
    <script src="app.js"></script>
  </body>
</html>

app.js

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

ctx.fillStyle = 'red';
ctx.fillRect(50, 20, 100, 100);

ctx.fillStyle = 'blue';
ctx.fillRect(20, 10, 90, 50);

效果

01.png

特性

现在我需要移除掉蓝色块,但Canvas没有DOM那样可以捕获蓝色块的Element并Remove就好,只能有一个做法

  • 先清除画布内容
  • 重新绘制

app.js

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

ctx.fillStyle = 'red';
ctx.fillRect(50, 20, 100, 100);

ctx.fillStyle = 'blue';
ctx.fillRect(20, 10, 90, 50);

ctx.clearRect(0, 0, 300, 200);

ctx.fillStyle = 'red';
ctx.fillRect(50, 20, 100, 100);

02.png

改进

Canvas因为没有对象管理,所以我们需要在程序层去对象化,使得之后可以对已绘制的对象做管理

  • 删除/隐藏操作
  • 样式改变

app.js

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

const renderReact = (x, y, width, height) => {
  ctx.fillRect(x, y, width, height);
};

const render = (arr) => {
  ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
  arr.forEach((item) => {
    ctx.fillStyle = item.color;

    if (item.type === 'rect') {
      renderReact(item.x, item.y, item.width, item.height);
    }
  });
};

const items = [
  {
    type: 'rect',
    color: 'red',
    x: 50,
    y: 20,
    width: 100,
    height: 100,
  },
  {
    type: 'rect',
    color: 'blue',
    x: 20,
    y: 10,
    width: 90,
    height: 50,
  },
];

render(items);

基于以上的模式,我们现在要删除掉蓝色块就很容易了,只需要一条语句

render(items.filter(item => item.color != 'blue'));

最后app.js

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

const renderReact = (x, y, width, height) => {
  ctx.fillRect(x, y, width, height);
};

const render = (arr) => {
  ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
  arr.forEach((item) => {
    ctx.fillStyle = item.color;

    if (item.type === 'rect') {
      renderReact(item.x, item.y, item.width, item.height);
    }
  });
};

const noBlue = (arr) => arr.filter(item => item.color !== 'blue');

const items = [
  {
    type: 'rect',
    color: 'red',
    x: 50,
    y: 20,
    width: 100,
    height: 100,
  },
  {
    type: 'rect',
    color: 'blue',
    x: 20,
    y: 10,
    width: 90,
    height: 50,
  },
];

render(items);

render(noBlue(items));

单元测试

既然以上的例子,我们通过对象化管理了Canvas,那么我们在测试的时候,其实只要保证items内容正确就Okay,因为items就会被渲染到画布,而render里调用的大多是Canvas的API,我们不需要关注调用API后浏览器是否真的渲染,因为浏览器对于Canvas的API已经经过各种场景的测试了,通常情况下你发现画布渲染的东西不是你想要的,那么99%有两种可能

  • 你的items给错了
  • 你的参数写错了

以上两种结果都能导致无法按照你的预期在渲染,很少说浏览器的API出现Bug导致你画的图不对,所以出现问题,请先从自己的业务代码找起来,那么我们可以这么测试

test.js


import { 
  noBlue,
  render,
} from 'app.js';

const items = [
  {
    type: 'rect',
    color: 'red',
    x: 50,
    y: 20,
    width: 100,
    height: 100,
  },
  {
    type: 'rect',
    color: 'blue',
    x: 20,
    y: 10,
    width: 90,
    height: 50,
  },
];

result = noBlue(items);

render(result);

test('Canvas应该有一个对象', () => {
  expect(result).toHaveLength(1);
});

test('验证这个对象信息', () => {
  expect(result[0]).toEqual(items[1]);
});

以上测试,看着没有测试Canvas的最终结果,但你的业务代码都测试到了,特别是noBlue函数,如果它不能按预期过滤,你的测试将会不通过,所以单元测试Canvas,实际上是在测试你的业务代码,你需要相信一点:如果你的业务代码没问题,那么Canvas会按照你的预期再渲染,有问题,绝大部分情况下要相信是你的业务代码出现问题,而不是Canvas出现Bug