Jest+Enzyme测试React组件(编码规范–>测试实例)(二)

时间:2020-9-12 作者:admin

5. 测试SearchForm表单组件

源代码:

import { Form } from 'antd'
import { WrappedFormUtils } from 'antd/lib/form/Form'
import * as React from 'react'

import { fetchList } from './RuleWhitepaper/helper'
import { DepartmentTree } from 'component/DepartmentTree'
...

interface IProps {
  form: WrappedFormUtils
}

export const SearchForm = (props: IProps) => {
  const { form } = props

  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    form.validateFields((error, values) => {
      promoOptionsPreview.searchQuery.set(values)
      fetchList(values)
    })
  }

  return (
    <Form layout="horizontal" onSubmit={onSubmit} {...FORM_ITEM_LAYOUT}>
      <ColWrapper>
        <DepartmentTree form={form} disallowSelectTopLevel multiple={false} selectableByChecked />
      </ColWrapper>
      <SubmitButton form={form} />
    </Form>
  )
}

export default Form.create()(SearchForm)

Form表单有一些需要注意的点

No.1 怎么样让测试语句也有form属性呢?

No.2 Form组件的实例是怎么获取的?

No.3 onSubmit提交时, 异步请求还没回来怎么办?

此时我的心里有一匹🐴

接下来, 让我们来一一解答

No.1 使用Form.create()修饰器无法获取组件实例😲

你一般会到用@Form.create()装饰器语法, 但是他可能会导致无法正常获取表单实例

@Form.create()
@inject('store')
@observer
export class SearchForm extends React.Component<IProps, IState> {
  ...
  public render() {
    return (
      <Card>
        <Form onSubmit={this.submit}>
           ...
        </Form>
      </Card>
    )
  }
}

export default SearchForm

因此,你需要把Form.create()修饰器改成函数式调用的方式:

interface IProps { // 注意这里
  store?: {
    app: AppStore
    department: DepartmentStore
    promoRules: PromoRulesStore
  }
  form: WrappedFormUtils
  wrappedComponentRef?: any
}

@inject('store')
@observer
export class SearchForm extends React.Component<IProps, IState> {
  ...
  public render() {
    return (
      <Card>
        <Form onSubmit={this.submit}>
           ...
        </Form>
      </Card>
    )
  }
}

export default Form.create<IProps>()(SearchForm) // 答案在这里

如果你现在使用的 TS,还需要把当前组件的props类型定义,传递给Form.create(),否则会抛出类型错误。因为antd要用它在内部进行更进一步的类型定义。

同理, 测试语句是这样的:

import React from 'react'
import { mount } from 'enzyme'
import { Provider } from 'mobx-react'
import { Form } from 'antd'

const Comp = Form.create()(({ form }) => <SearchForm form={form} />)

const wrap = () =>
  mount(
    <Provider store={store}>
      <Comp />
    </Provider>,
  )

No.2 现在我们的Form组件已经有了this.props.form属性, 但是组件实例是怎么得到的呢, 哈哈, 聪明的你一定想到了 wrappedComponentRef这个高阶组件。

antd官网上面提到过, 使用 rc-form提供的 wrappedComponentRef, 可以拿到 ref

antd Form

测试语句:

const wrapper = () =>
  mount(
    <Provider store={store}>
      <SearchForm
        // 看这里
        wrappedComponentRef={(formEle: any) => { 
          formInstance = formEle
        }}
      />
    </Provider>,
  )

使用wrappedComponentRef属性,指定一个回调函数,通过它的回调参数即可拿到当前的表单实例。

注意: 一般表单触发的submit方法都会有一个默认的回调参数对象,它里面包含了很多浏览器原生方法和属性。(比如event)

所以, 如果你的源代码中用到了相关的属性,我们在测试的时候必须对它进行一个模拟,否则源代码肯定会抛异常。

formInstance.submit({
  preventDefault: jest.fn(), // jest.fn表示返回一个空函数
})

具体测试语句:

it('测试submit方法', done => {
  const spy = jest.spyOn(formInstance, 'fetchData')
  // 这里是不是很react😀
  formInstance.props.form.setFieldsValue({ 
    deptId: undefined,
  })

  formInstance.submit({
    preventDefault: jest.fn(),
  })

  setTimeout(() => { // setTimeout, 这里我懂了💡
    expect(spy).not.toHaveBeenCalledTimes(1)
    done() // 这里是什么🐷
  })
})

No.3 上面的代码正好回答了我们的第三个问题

因为antd form对象上的setFieldsValue方法是异步的。所以,这里一般我会加一个setTimeout。否则,测试用例可能一直无法测试成功。

done方法用于解决异步代码测试问题, 在一个异步语句中, 你的测试将会在调用回调之前完成。所以, 使用一个名为 done的参数。jest将等待回调完成后进行测试。

6. 测试helper.ts

helper文件里存放的都是页面的交互方法, 如果你用的纯函数的化, 只需要传入不同数据, 测试即可。

源代码:

// 这里需要有个event, 浏览器原生属性
export const filter = (form: WrappedFormUtils) => (e: React.FormEvent<HTMLFormElement>) => { 
  e.preventDefault() // 注意这里
  form.validateFields(async (err, values) => {
    if (!err) {
      const thresholdFilter = formatFormValuesToGroup(values)
      const query = {
        deptLevel: values.deptLevel,
        cidLevel: values.cidLevel,
        deptId: values.deptId,
        cid: values.cid,
        deptName: values.deptName,
      }
      if (isEmpty(values.cid)) {
        // 删除对象属性用es6中的Reflect
        Reflect.deleteProperty(query, 'cid') 
        Reflect.deleteProperty(query, 'cidLevel')
      }
      await store.thresholdRange.fetch({ body: { ...query } })
      store.sliderScope.set(store.thresholdRange.value.minAndMaxThreshold as number[])
    }
  })
}

React的合成事件系统只是原生DOM事件系统的一个子集, 它仅仅实现了DOM level3的事件接口, 并且统一了浏览器间的兼容性问题。

有些事件React并没有实现, 或者受某些限制没办法去实现, 比如window的resize事件。

《深入React技术栈》

这里着重讲一下方法里面传入form属性时, 该如何写测试语句。

测试代码:

it('filter,过滤金额门槛', async () => {
  const values = {
    promoDataTimeScope: 'half_year',
    deptLevel: 2,
    deptId: '837',
    cidLevel: 12,
    cid: '',
    deptName: 'abc',
  }
  const form = ({ 
    // 在ts里面validateFields是必传项, 这里mock一个假函数
    validateFields: jest.fn(cb => {
      // 模拟 没有错误提交
      cb(null, values)
    }),
  } as unknown) as WrappedFormUtils
  // 同理, event也需要mock
  const e = ({ preventDefault: jest.fn() } as unknown) as React.FormEvent<HTMLFormElement>
  // 这里spyOn后端的请求接口 ❗
  const spy = jest.spyOn(store.thresholdRange, 'fetch').mockImplementation(() => Promise.resolve() as Promise<any>)
  await filter(form)(e)
  expect(spy).toHaveBeenCalled() //❗
  expect(store.sliderScope.value).toEqual([])
})

注意: 上述代码有一个语句前后顺序问题

const spy = jest.spyOn...
expect(spy).toHaveBeenCalled()

jest.spyOn一定要写在前面, 否则测试不通过, 因为 store.thresholdRange还没有被请求。

// error
expected: >=1
received: 0

未完待续…

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。