任务 3:组件级别的快速反馈

Hey,你好,我是 JimmyLv 吕靖,这是你开始 前端 TDD 练习的第 13 天,今天我们会接着练习 CommentBox 项目,学习如何使用 Storybook 来测试组件样式。

练习目标

在之前的练习中,我们还没有涉及到组件样式,但其实在前端工作中大部分时间是消耗在了调整 CSS 样式上面,可能你也有过如下“内心崩溃”的经历。

这一次,我们对页面及组件的样式有严格的要求,具体的评论框样式可参考 YouTube(需要梯子),或 Ant Design UI 组件库,需要尽可能做到 100% 像素级还原。

yarn stroybook #启动组件运行时
yarn test #运行单元测试
yarn test:story #运行组件快照测试

本次任务的练习要求

  • 使用独立的 Storybook 组件运行时,调整样式

  • 尽可能穷尽组件的所有使用场景,一种情况一个 story

  • 完成所有情况后,使用组件快照测试自动化回归组件样式

  • 别忘了重新运行 Cypress E2E 测试,看是否破坏原有功能

Storybook 组件运行时

Storybook: UI component explorer for frontend developers

通过 CDD(Components-Driven-Development)的方式在 React Storybook 里边儿写 components.stories.js,即人为验证,视觉驱动出来的组件代码,在样式上的反馈,人眼反而是最快的。

验证组件的 UI 样式

#Counter.stories.js
export default { title: 'Counter' }
export const enabled = () => (
  <Counter text="Enabled" />
);
export const disabled = () => (
  <Counter disabled text="Disabled" />
);

在测试中使用 Storybook 用例

这是一个额外的福利,当我们在测试用例中 render 一个组件后,可以将它提取出来,放到单独的 Component.stories.js 文件中,从而省掉了重新定义 Storybook story case 的代码。

#Button.test.js
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { enabled, disabled } from './Button.stories';
test('should increment when enabled', () => {
    const comp = render(enabled());
    userEvent.click(comp.getByText('Enabled: 0'));
    expect(comp.getByText('Enabled: 1')).toBeTruthy();
});
 test('should do nothing when disabled', () => {
    const comp = render(disabled());
    userEvent.click(comp.getByText('Disabled: 0'));
    expect(comp.getByText('Disabled: 0')).toBeTruthy();
 });

Storyshot 快照测试

写完了组件样式,让我们通过视觉测试给 UI 组件加上一个安全保护伞。只需要给 UI Components 的 Stories 照个相就可以啦,结合 Storyshots 自动生成 Snapshot Testing,成本几乎为 0。

import initStoryshots from '@storybook/addon-storyshots'
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer'

const beforeScreenshot = (page, { context: { kind, story }, url }) => {
  page.setViewport({ width: 1440, height: 960 })
}

initStoryshots({
  suite: 'Image storyshots',
  test: imageSnapshot({
    storybookUrl: 'http://localhost:6006',
    beforeScreenshot,
  }),
})

结合 Storybook 引入 Storyshot,自动在 UI 完毕之后 take Snapshot,而且是图片级别的快照。

TDD 与视觉测试保护伞

然后,在新的需求变更或是 CSS 重构之后,我们可以通过 Storyshot 自动打个快照,截图对比:

综上,个人认为 React Components 应当尽可能保持纯净,就安安心心做自己的 V(iew) 不好吗?lifecycle 什么的就不要 care 啦,现在使用 React Hook 便可非常直接得抽取逻辑,保持 functional component 纯净。而 pure UI 的组件,自然可以通过 Snapshot Testing 的方式轻松验证每次更改的合法与否。

import React, { useState } from 'react'

function Example() {
  // 声明一个新的叫做 “count” 的 state 变量
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

与其改动测试代码与实现代码,不如在 CDD 直接改动实现代码并验证过后,重新给 UI 组件照个相然后替换原有的相片即可,这样就能用新的相片(baseline)去验证未来可能不小心改动所影响的 break change,也就达到了 Testing 最初的目的。

左边 vs 右边,中间是 diff 的不同内容!

至此,「驱动」和「验证」两大目的均已达到,并能在一定程度上减少「成本」与提高「速度」。

而完全抽离出来的 Redux 逻辑才是真正与 data 相关的地方,absolutely 值得一测并且应当尽可能提高测试覆盖率。再者,在合理抽象与充分隔离的基础上,pure FP 测试起来也是一件非常 easy 和享受的事情,这个留到后边儿的练习项目再说。

对于前端来说,TDD 业务逻辑和交互行为是最值得去做的,也是最容易去实施 TDD 的,你要学会有的放矢,把力气使在刀刃上,减少浪费。保持组件尽可能 pure component,然后组件样式什么的,就不要去傻傻得验证几个 px 啦,就直接通过 CDD 在 Storybook 里面看就好了。而图片快照和全局的 visual testing 会给我最直观的反馈和最细致的对比,截图比什么都来得快、来得直观。

在这儿还可以稍微脑洞一下,最极端的情况,追求像素级还原:视觉测试,可以直接拿设计稿来对比嘛!

也请你思考一下

  • 独立的 UI 组件运行时带来了什么好处?

  • 前端 TDD 的不同阶段,侧重点分别是什么?

  • 整理你的窗口布局,让反馈更加一触即达(IDE、命令行、浏览器、设计稿…)

然后……作为前端开发者,或许你需要第二块显示器屏幕。

最后更新于