React全家桶与前端单元测试艺术

发表于:2017-9-08 10:42

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:王亦凡    来源:51Testing软件测试网采编

分享:
  组件测试 (View测试)
  React是一个View library,它干的活就是DOM domain里的两个事:渲染和捕获事件。我们在这里依然从简,只用stateless component这个子集,虽然在用到生命周期方法的时候需要用一下class,但绝大多数时候应该只用stateless component。
  它以Virtual DOM的形式封装了恶心的浏览器基础设施,让我们以函数和数据结构来描述组件,所以和大部分框架不同,我们的测试依然可以在node上并行运行。如果用Karma + Chrome真正地渲染测试,你会发现共享一个浏览器实例的测试非常慢,几乎无法watch测试,因此我们的TDD cycle就会变得不那么流畅了。
  最基本的就是state => UI这种纯函数组件:
  const Greeter = ({ name }) => <p>Greetings {name}!</p>
  使用的时候就像HTML一样传递attribute就可以了。
  render(<Greeter name="React"/>, document.body)
  最简单的测试还是判等,我们用一个叫jsx-test-helpers的库来帮我们渲染:
  import { renderJSX, JSX } from 'jsx-test-helpers'
  const Paragraph = ({ children }) => <p>{children}</p>
  const Greeter = ({ name }) => <Paragraph>Greetings {name}!</Paragraph>
  test('Greeter', t => {
    t.is(renderJSX(<Greeter name="React"/>), 
         JSX(<Paragraph>Greetings React!</Paragraph>), 
         'should render greeting text with name')
  })
  这里我多加了一层叫做Paragraph的组件,它的作用仅仅是传递给p标签,children这个prop表示XML标签传进来的子元素。多加这层Paragraph是为了展示renderJSX只向下渲染了一层,而非最终需要渲染的p标签。这样我们在View上的测试粒度就会变得更小,成本更低,速度更快。
  View不像业务本身那么稳定,细粒度低成本的快速测试更划算些,这也是为什么我们的View都只是接受参数渲染,这样你只用测很少的case就能保证View可以正确渲染。假如你的FSM Model有M种可能性,View显示的逻辑有N种,如果将两个集成在一起测试可能就需要M×N种Path,如果分开测就有M+N种。View和Model的边界清晰时,你的Model测试不容易被更困难的View测试干扰,View测试也减少了混沌程度,需要测试的情形就减少了。
  我们的组件不应该只有渲染,还有事件,比如我们封装个TextField组件:
  const TextField = ({ label, onChange }) => <label>
    {label}
    <input type="text" onChange={onChange} />
  </label>
  当然我们还可以判等,只要onChange函数引用相同就好了。
  test('TextField', t => {
    const onChange = () => {}
    const actual = renderJSX(<TextField label="Email" onChange={onChange} />)
    const expected = JSX(<label>
      Email
      <input type="text" onChange={onChange}/>
    </label>)
    t.is(actual, expected)
  })
  当然有时候你的组件更复杂些,测试时并不关心组件是不是完全按你想要的样子渲染,可能你想像jQuery一样选择什么,触发什么。这样可以用更主流的enzyme来测试:
  import {shallow} from 'enzyme'
  import sinon from 'sinon'
  test('TextField with enzyme', t => {
    const onChange = sinon.spy()
    const wrapper = shallow(<TextField label="Email" onChange={onChange} />)
    t.true(wrapper.contains(<label>Email</label>), 'should render label')
    const event = { target: { value: 'foo@bar.com' } }
    wrapper.find('input').simulate('change', event)
    t.true(onChange.calledWith(event))
  })
  这里用的shallow顾名思义,也是向下渲染一层。此外我们还用了spy,这样测试就变得有点复杂了,丢掉了我们之前声明式的优雅,所以组件还是小一点、一下测完比较好。
  还不够快?Facebook就觉得不够快,他们觉得View测试成本比较浪费,干脆搞了个Snapshot测试——意思就是照个像,只断言它不变。下次谁改了别的地方不小心影响到这里,就会挂掉,如果无意的就修好,如果有意的话和git一样commit一下就修好了:
  import render from 'react-test-renderer'
  test('Greeter', t => {
    const tree = render.create(<Greeter name="React"/>).toJSON()
    t.snapshot(tree, 'should not change')
  })
  当你修改Greeter的时候,测试就会挂掉,这时候运行:
  ava --update-snapshots
  就好了。Facebook自家的Jest对snapshot的支持更好,当snapshot不匹配时按个y/n就完事了,够快了吧。要有更快的可能就是不测了……
  小结
  这节里我们展示了3种测试View的不同方式,它们都比传统框架更简单更快速。我们的思路还是以判等为主,但不同于Model,粒度越大越好。View测试粒度越小越好,足够小、足够幂等之后,其实不用测试你也可以发现组件总是按照预期工作。相比之下MVVM天然有一种让View和Model粒度拟合的倾向,很容易让测试变得既难测又缺乏价值。
  异步Effect测试
  这算个续集……异步操作不复杂的项目可以无视这段,可以选择性不测。
  React先解决了恶心的DOM问题,把Model的问题留下了。然后Redux把同步逻辑解决了,其实前端还留下异步操作的大问题没有解决。这种类似“Unix只做一件事”的哲学是React全家桶的根基。我们用一个叫做Redux-saga的库来展现全家桶的异步测试怎么写,Redux模仿的目标是Elm architecture,但是简化掉了Elm的作用模型,只保留了同步模型,Redux-saga其实就是把Elm的作用模型又拿回来了。
  Saga是一种worker模式,很早之前在Java社区就存在了。Redux-saga抽象出来多种通用的作用比如call / takeEvery等等,然后有了这些作用,我们又可以愉快地判等了。比如:
  import { takeEvery, put, call, fork, cancel } from 'redux-saga/effects'
  function *account() {
    yield call(takeEvery, 'login/REQUESTED', login)
  }
  function *login({ name, password }) {
    try {
      const { token } = yield call(fetch, '/login', { method: 'POST', body: { name, password } })
      yield put({ type: 'login/SUCCEEDED', token })
    }
    catch (error) {
      yield put ({ type: 'login/FAILED', error })
    }
  }
  这段代码乍看起来很丑,这是因为它把程序里所有异步操作全都集中在自己身上了。其他部分都可以开心地发同步事件了,此外有了Saga之后Redux终于有了“用事件触发事件”的机制了,只用redux,应用复杂到一定程度你一定会想这个问题的。
  这是个最普通的API处理saga,一个account worker看到每个’login/REQUESTED’就会forward给login worker(takeEvery),让它继续管下面的事。然后login worker拿到消息就会去发请求(call),之后傻傻地等着回复,或者是出错。最后它会发出和结果相关的事件。用这个方式你可以轻松解决疯狂难度的异步问题。
  test('account saga', t => {
    const gen = account()
    t.deepEqual(gen.next().value, call(takeEvery, 'login/REQUESTED', login))
  })
  test('login saga', t => {
    const gen = login({ name: 'John', password: 'super-secret-123'})
    const request = gen.next().value
    t.deepEqual(request, call(fetch, '/login', { method: 'POST', body: { name: 'John', password: 'super-secret-123'} }))
    const response = gen.next({ token: 'non-human-readable-token' }).value
    t.deepEqual(response, put({ type: 'login/SUCCEEDED', token: 'non-human-readable-token' }))
    const failure = gen.throw('You code just exploded!').value
    t.deepEqual(failure, put({ type: 'login/FAILED', error: 'You code just exploded!'}))
  })
  你看我们的测试连异步操作都还可以无耻地判等。call就是以某些参数调用某个函数,put就是发事件。
  可以试着把fetch覆盖成空函数,你可以发现实际上副作用根本没发生,“fetch到底是个啥”对测试一点影响都没有。你可能发现了,其实saga就是用数据结构表示作用,而不着急执行,在这里又走回幂等的老路了。这和React Virtual DOM的思路异曲同工。
  结语
  首先是文章开头提到的TL;DR的内容。函数是个好东西,测函数不等同“测1+1=2”这种没营养的单元,函数是可以包含很大上下文的。这种输入输出的模型既简单又有效。
  我们消灭了mock,减少了依赖,并发了测试,加快了速度,降低了门槛,减少了测试路径等等。如果你的React项目原来在TDD的边缘摇摆不定,现在是时候入一发这种唯快不破了。
  全家桶让Model/View/Async这三者之间的边界变得清晰,任由业务变更,它们之间的职责是不会互相替代的,这样你测它们的时候才更容易。后端之所以测试稳定是因为有API。所以想让前端好测也是一样的思路。
  文中好多次提到“幂等”这个概念,幂等可以让你减少测试的case,写代码更有底气。抛开测试不谈,代码幂等的地方越多,程序越可控可预期。其实仔细思考一下我们的实际项目,大部分业务都是非常确定的,并没有什么随机因素。为什么最后还是会出现很多随机现象呢?
  声明优于命令,描述发生什么、想要什么比亲自指导具体步骤好。
  消息机制优于调用机制。Smalltalk > Simula。其实RESTful API一定程度上也是消息。简单的对象直接互相作用是完全没问题的,人作为复杂对象主要通过语言媒介来交流,听到内容思考其中的含义,而不是靠肢体接触,或者像连体婴儿那样共享器官。所以才有一句俗语叫“你的对象都想成长为Actor”。
  从View的几种测试里我们也可以看到,测试并不是只有测或者不测这两种选择,我们老提测试金字塔,意思是测试可多可少,不同层级的测试保持正金字塔形状比较健康,像今天我们说的就可以大幅加宽你测试金字塔的底座。所以你的项目有可能测试过少,也可能测试过度,所以时间可以动态调整。
  没用全家桶的项目可以把“大Model小View”的思想拿走,这样更容易于专注价值。尽量抽出Model层,不要把逻辑写在VM里,看那样似省事,行数在测试里都还回来了。
22/2<12
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号