jimmylv.github.io
jimmylv.github.io copied to clipboard
FED UI Testing & Refactoring 的痛点与反思
项目纵向拆分
前端UI级别单元测试的一些痛点:
- class name老变来变去的,依赖css selector的测试都得改
- UI styling根本没法测,多个少个px过分了
- 很多跨browser的hack,不同实现还得写不同测试cover?
- 响应式是个什么鬼?为什么天底下有这么多种不同分辨率的屏幕!?
- ……
又想起了大熊写的「重构已死」,讲的就是"重构本质乃是不改变软件可观察行为与功能"这个前提不再成立,因为软件本身就无时无刻随着需求的快速变化而改变,那何来不改变的软件行为呢?同理运用到测试上来说,需求老变化,每次都得改测试可烦了,PM/UX还没事儿跟你说这儿多个px,那儿少个icon之类的…
总之结论就是:TDD, 重构, 测试这些方法论运用在 后端业务需求,领域建模核心上来说很好使,这些东西也是从那时候开始发展而来…但是偏偏这个商业时代各种客户端UI层出不穷,需求变动也比以往大得多,很多APP都是周更新,那这些方法论也遇到了一些矛盾的地方
因此更讲究避免浪费吧,然后就来谈精益,🤣 所以我还是力挺TDD,而测试只是TDD的附属品。另外一个观点是:如果需求变动过大,我不如重新TDD实现一遍;而不是在原来的代码(&测试)基础之上进行重构。
需求变化,变化过快,这个真的会让测试带来的价值变少,甚至成为一种浪费吗?单元测试的输入,本身就是基于我们对设计和实现方案的假设,接口变了,需求变了,测试跟着变是当然的,只不过是假设的输入变了。说需求变了,因而测试价值减少了,因而原来就最好不写减少浪费,那变后的这些需求你又用什么来验证呢?还不是回到没测试裸奔手工验的状态。 我最近在开发,写好的单元测试,输入也是经常变,但原来的测试是不是就没有价值了?当然不是,测试挂掉的所有用例会帮我找出所有调用点。我不需要自己再去手动找,这个变的接口影响了哪些地方,我也不想找。把测试输入改成变后的需求,运行测试,挂掉的全部修好,然后再真实起应用跑,基本全是好的,就是不是,debug起来也很轻松。如果说抱怨测试经常要改输入改输出,那没有了测试接口需求变起来,验得岂不是更惨? 这段逻辑,主要说的是强逻辑强数据的代码段。ui方面,我觉得你说的痛点都存在,那种情况下,可能是自动化测试的成本,已经大过了手动验证或cdd的成本。「不改变的软件行为」,我觉得并不是指在一个月两个月内的软件行为都不变,而是你「重构前」和「重构后」的软件行为不能变
嗯 我都同意,特别是有明显输入输出和数据强相关的地方。而且你发现了没有,这也是React所带来的好处,哈哈哈,数据驱动和函数式思想(纯I/O),也让测试变得简单、单元化。衡量成本这件事情还是很难量化,感性得说,我还是不喜欢写UI测试…只写数据相关的测试会很爽,那么如果UI=f(data)这个等式在React中被严格实施的话,把data控制好测好,我觉得最终的UI也一定是好的,不测也罢。
上次看朋友圈有人吐槽Vue没法写测试,会心一笑 🙃
讲道理。我其实是并不赞同所谓的“测试”前端UI。如果像JSX这样的需要一次编译的,完全可以加强一下编译器,如果没有编译错误或者警告,就可以认为是正确的。
对呀,“如果UI=f(data)这个等式在React中被严格实施的话,把data控制好测好,我觉得最终由 jsx compile 出来的UI也一定是好的,不测也罢”
试问在什么情况下你的component里面会有复杂的业务逻辑导致你根本不能正确的写一段清晰的结构
组件的分类(Vue 作者尤大分享的 live):
- 接入型 container
- 展示型
- 交互型 比如各类加强版的表单组件,通常强调复用
- 功能型 比如
<router-view>
,<transition>
,作为一种扩展、抽象机制存在。(high-order component 高阶组件)
⓵ 定义目标和原则
⓶ 展望结果
⓷ 头脑风暴
⓸ 组织整理
⓹ 明确「下一步行动」
不会写就没法写呗。(会心一笑
quote without referring 🙃, 不开心。
- 消除 ContainerComponents
- 1.1 转移
dispatch()
logic 到 route 或者全局- 1.1.1 object literal shorthand for binding actions with connect
- 1.2 不要用
@connect()
decorator,因为不需要React.Component
class - 1.3 => only connect redux store with FP UI components
- 1.4 => no
mount()
to invoke lifecycle method with Enzyme
- UT for Redux (side effort, business logic)
- 2.1 reducers
(state, action) => state
还可以借助 Redux DevTool 自动生成 UT - 2.2 actionCreators
- 2.2.1 测试异步逻辑 =>
redux-mock-store
+redux-thunk
middleware - 2.2.2 使用
redux-saga
Pros/cons of using redux-saga with ES6 generators vs redux-thunk with ES2017 async/await - Stack Overflow
- 2.2.1 测试异步逻辑 =>
- 2.3 selectors
- 在上面的基础上,我们再来谈一谈 Jest 的 Snapshot Testing
- 3.1 纯 Stateless UI Components 需要单元测试吗? -> 至少价值很少
- 3.2 需要维护内部 state 的复杂交互 UI Components(Modal、Form 等)-> 当然用第三方库啊(
react-modal
etc) - 3.3 已经通过 CDD(Components-Driven-Development)的方式在 React Storybook 里边儿写过
components.stories.js
,即人为验证,视觉驱动出来的组件代码。 - 3.4 => 当然只需要给 UI Components 的 Stories 照个相就可以啦,结合 Storyshots 自动生成 Snapshot Testing,成本几乎为 0。
综上,个人认为 React Components 应当尽可能保持纯净,就安安心心做自己的 V(iew) 不好吗?lifecycle 什么的就不要 care 啦。而 pure UI 的组件,自然可以通过 Snapshot Testing 的方式轻松验证每次更改的合法与否。与其改动测试代码与实现代码,不如在 CDD 直接改动实现代码并验证过后,重新给 UI 组件照个相然后替换原有的相片即可,这样就能用新的相片(baseline)去验证未来可能不小心改动所影响的 break change,也就达到了 Testing 最初的目的。
至此,「驱动」和「验证」两大目的均已达到,并能在一定程度上减少「成本」与提高「速度」。
而完全抽离出来的 Redux 逻辑才是真正与 data 相关的地方,absolutely 值得一测并且应当尽可能提高测试覆盖率。再者,在合理抽象与充分隔离的基础上,pure FP 测试起来也是一件非常 easy 和享受的事情。
理想的(简易版)的 React、Redux 文件目录组织
src/
index.js -> ReduxProdvider, ThemeProvider, ...
routes.js
*index.css -> global styling (or use styled-components)
components/
Header/
index.js
*index.test.js -> 可以用 Storyshots 做 Snapshot Testing
index.stories.js
Content/
index.js
*index.test.js -> 可以用 Storyshots 做 Snapshot Testing
index.stories.js
ducks/
clients/
profile/
index.js
index.test.js
product/
index.js
index.test.js
account/
index.js
index.test.js
selectors/
profile/
index.js
index.test.js
product/
index.js
index.test.js
account/
index.js
index.test.js
index.js -> setup Redux store(apply middlewares, combine reducers)
profile.js -> export actionCreators, export default reducer
profile.test.js -> test actions, reducers
product.js
product.test.js
account.js
account.test.js
shared/
models/
prop-types.js
profile.js
product.js
account.js
utils/
testHelper.js
otherOptions.js
特别好的一个,在 React 和组件化背景下的 Visual Testing 文档: visual testing handbook.pdf
Chroma 这家公司就是维护 React Storybook 的公司,现在在搞一个成套的解决方案,Introducing Chromatic — UI testing for React – Chroma
就冲这句话,Say bye to UI bugs. 😂
再次推荐这一系列的好文章,UI Components – Chroma
消除 container components 是一种倒退,组件化将会失去意义,container + Redux store 也会变得混乱。
UI Testing 就不该只 focus 在组件单元级别,吃力不讨好。
参考 测试奖杯 🏆 via https://github.com/JimmyLv/jimmylv.github.io/issues/278#issuecomment-495134516
兼容性测试,也需要考虑。IE11 and Firefox now in Chromatic - Chroma
总结一下 Chromatic 作为 SasS 的最佳实践:
- Frontend teams Ship features smarter
Automating visual QA helps you ship bulletproof UIs faster and more efficiently.
- Design systems Ensure UI consistency
Testing components ensures a consistent look & feel at every touchpoint.
- Digital agencies Deliver projects faster
Chromatic automates visual review so you can dodge expensive bugs, avoid rework, and move fast.
特别是对 Snapshot Testing 的回答:
Isn’t this just snapshot testing?
Not quite. Jest snapshot testing captures your component’s HTML output. Chromatic captures a pixel-perfect image. Chromatic is best suited for testing UIs for these reasons:
- The way the browser renders UI is also determined by styling and assets such as images. Unlike Jest snapshots, Chromatic renders markup, styling, and assets in a browser to capture what users really see.
- Changes are not necessarily “failures”. Deciphering intentional changes from bugs is hard when all you have is a markup diff. Chromatic’s online merge manager helps you visualize, review, and merge UI changes.
- Code changes don’t always result in UI changes. Jest snapshot tests only identify changes in code. This ends up leading to a lot of false positives and resorting to “approve all” to make test failures go away. Chromatic does not have this problem.But what about Jest image snapshot? Image snapshotting is one of Chromatic’s features. With Chromatic you also get cloud performance, an online review flow, no-hassle branching, painless merging, component history, and more work-saving features for professional developers.
"Snapshot testing can be useless, or super useful. Your choice.
- babel-plugin-tester
- snapshot-diff One of the most useful things that I've found with test maintainability is when you have many tests that look the same, try to make their differences stand out. " Effective Snapshot Testing - Kent C. Dodds
const React = require('react')
const {toMatchDiffSnapshot} = require('snapshot-diff')
const Component = require('./Component')
expect.extend({toMatchDiffSnapshot})
test('snapshot difference between 2 React components state', () => {
expect(<Component test="say" />).toMatchDiffSnapshot(
<Component test="my name" />,
)
})
Example:
First, you write a test, calling .toMatchInlineSnapshot() with no arguments:
it('renders correctly', () => {
const tree = renderer
.create(<Link page="https://prettier.io">Prettier</Link>)
.toJSON();
expect(tree).toMatchInlineSnapshot();
});
The next time you run Jest, tree will be evaluated, and a snapshot will be written as an argument to toMatchInlineSnapshot:
it('renders correctly', () => {
const tree = renderer
.create(<Link page="https://prettier.io">Prettier</Link>)
.toJSON();
expect(tree).toMatchInlineSnapshot(`
<a
className="normal"
href="https://prettier.io"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Prettier
</a>
`);
});
当然 Snapshot 测试本身,也在进步。
摘录自 Introduction: React UI Testing
Reasons for Testing:
- To find bugs.
- To make sure things won’t break between new code commits.
- To keep tests as living documentations.
Different Aspects of UI Testing:
- Structural Testing (判断元素是否存在即可,Snapshot 足以,但不如跟
3.
结合,直接用 Storybook + Storyshots) - Interaction Testing (组件交互行为,UI is all about interacting with the user. 通常跟 Testing-Library 来模拟用户行为,不过注意是单元层面,TDD assertion 风格)
- CSS/Style Testing (组件样式,通常 based on Design 即可,或者引入设计系统组件库,UI is all about styles (whether they’re simple, beautiful, or even ugly).)
- Manual Testing (E2E Cypress 来自动化处理主流程,同时可视化出来进行人为 check,通过是 BDD assertion 风格)