1. React functional components

Fb team recommends the use of functional components for development, but functions are stateless, it is not nice to use the class component, with its own state, why to write 😧

That seems to be a good answer

Why does React implement functional components now? Is it bad to use class?

1. Hooks are more elegant ways to reuse logic than HOC and render props

State is an external data

The state obtained by useState is external data for components. It is the same as props or context. The states declared by useState are actually maintained by the React kernel and passed to functional components. Function components of the hooks era are still pure functions of external data =>view.

The Ali team’s open source set of Hooks methods (now upgraded to Ahooks) is the LoDash of Hooks, sufficient for daily development and custom Hooks for complex businesses

ahooks

2. The mental model of functional components is more “declarative”

Hooks now give you an API to declare side effects directly, making the lifecycle a “low-level concept” without development concerns, developers working at a higher level of abstraction

3. Pure function components are a prerequisite for enabling “concurrent mode”

The React rendering process is essentially about calculating the view’s content based on the data model (application state).

After the component is purified, the developer writes the component tree as a pure function of the (application state)=>DOM structure. Because the React kernel actually maintains application state, the React kernel can maintain multiple data models and render multiple versions of the component tree concurrently. React developers only need to write pure functions and don’t need to worry about dealing with these concurrent renders.

Since reading the “JavaScript Functional Programming Guide” I feel that I learned how to slay the dragon, if all development is functional, the world would be a wonderful place 😀, but, until I came across a business problem and realized that the class component implementation is better 😏, young, too naive

Wait, not to say Jest test, talk about so much functional programming is what ghost 😵

Because writing unit tests has a lot to do with your component partitioning, logic reuse, and state transfer, if your code isn’t standardized, your code will be highly coupled, as in this case

// antd
{
  title: 'state'.dataIndex: 'optionStatus'.render: (value: number) = > (value: number) = > {
    if (value === 0) {
      return <Tag color="red">{PROPOSAL_RULE_ENABLE[0].label}</Tag>
    }
    if (value === 1) {
      return <Tag color="blue">{PROPOSAL_RULE_ENABLE[1].label}</Tag>
    }
    return <Tag color="blue">{PROPOSAL_RULE_ENABLE[2].label}</Tag>
  },
  width: 120,},Copy the code

The if conditional branch is easy to miss when writing test statements, and the logic is written in the render method, which mocks different data in the table, adding coding effort.

For example, if you write code with a combat value of 80 and write unit tests with a combat value of 120+, you must first understand your code logic to write good test statements.

And in the development to consciously think of the back of the test how to write, regardless of the end is very easy to rework code, of course, you say I don’t measure all doesn’t matter, a shuttle ha go to the end 🙉, so can also, but the back of the test may find you talk about 😂

This leads to the second question, how to divide my components according to the product prototype, how to design the code structure 🤔

2. Divide components

React + Typescript + Mobx + hooks

The above is the technology stack used in daily development of my department, for example 🌰

According to the above product prototype, split functional modules are as follows:

Functional component coding development:

Overview department. TSX // Search for Department components helper.ts // Add, delete, change, and search for change records by calling index.tsx // Skeleton List // Upper search componentCopy the code

When we wrote the code, the business modules were fully split, each function in a separate file, the entry file is not too heavy business code.

The Department can be placed in the Component folder, the helper file holds the page’s calling methods and interactions with the Store, and the rest of the components just render the page UI.

There is a granularity issue involved in making sure that each file addresses a single problem, such as data formatting can be placed in the Tool folder, and the code logic involved in individual files is not too heavy after the page is broken up. In subsequent tests, the files are tested as functions, with different parameters to be entered.

3. Test the index entry file

Example is the home page with TAB switch, without further ado, above code:

import React from 'react'.const PromoRule = observer(() = > {
  return (
    <Spin>
      <Tabs activeKey={PromoRule.tabValue.value} onChange={onChange}>
        <TabPane tab="Sales increase" key="GMVPromote">
          <GmvPromote />
        </TabPane>
        <TabPane tab="ROI ascension" key="ROIPromote">
          <ROIPromote />
        </TabPane>
        <TabPane tab="Double lift" key="DoublePromote">
          <DoublePromote />
        </TabPane>
      </Tabs>
    </Spin>)})export default PromoRule
Copy the code

Test code:

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

import { PromoRule } from 'page/PromoRules/OptionsCenter/Whitepaper/PromoRule'
import { PromoRule as store } from 'store/PromoRule'.const wrap = () = >
  mount(
    // 1. Inject the store required by the component
    <Provider store={store}>  
      <PromoRule />
    </Provider>,
  )

describe('page/PromoRules/OptionsCenter/WhitePaper/PromoRule'.() = > {
  it('Test renders page structure correctly'.() = > {
    const app = wrap()
    // 2. Check whether the component exists on the page
    expect(app.find(GmvPromote)).toHaveLength(1) 
    expect(app.find(ROIPromote)).toHaveLength(1)
    expect(app.find(DoublePromote)).toHaveLength(1)
    expect(app.find(Tabs).prop('onChange')).toBe(onChange)
    // 3. After each statement block is executed, unmount() and unmount it
    app.unmount() 
  })

  it('Test TAB toggle display correct'.() = > {
    const app = wrap()
    store.tabValue.set('ROIPromote')
    // 4. update() to re-render the page, otherwise does not take effect
    app.update() 
    expect(app.find(ROIPromote)).toHaveLength(1)
    app.unmount()
  })
})
Copy the code

If it’s too much trouble to wrap() and unmount() every time, you can write this

import * as React from 'react'
import { mount } from 'enzyme'
import { Provider } from 'mobx-react'. describe('page/PromoRules/Overview/AmountModal'.() = > {
  const wrap = () = >
    mount(
      <Provider store={store}>
        <AmountModal />
      </Provider>.)// 1. Type definition
  let app: ReturnType<typeof mount>
  
  beforeEach(() = > {
  	// 2. Note the order of the following statements
    promoOverview.amountModal.show()
    app = wrap()
  })
  // 3. beforeEach & afterEach
  afterEach(() = > {
    app.unmount()
    // 4. Modal box modal
    promoOverview.amountModal.hide()
  })

  it('Tests contain necessary subcomponents'.() = > {
    expect(app.find(ExportData)).toHaveLength(1)... })})Copy the code

BeforeEach and afterEach are executed before and afterEach it statement for repeated testing; This corresponds to beforeAll and afterAll, which are executed before and afterAll statements.

Scope: When the before and after statements are inside the Describe block, they only apply to tests within that describe block.

Note: in the modal box test, must first show the following, otherwise it is not measured 😂

4. Test the List table component

The development of the project uses the TABLE component in ANTD, and the data is returned from the back end. We only need to test the attributes in the table.

The source code:

import { ThresholdDetailVO } from 'service/promoRule/definitions'
import { randomRowKey } from 'tool/randomRowKey'.const columns: IColumnProps<ThresholdDetailVO>[] = [
  {
    title: renderColTitle,
    dataIndex: 'thresholdValue'.align: 'right'.render: (value, record) = > renderColValue(value, record),
  },
  {
    title: 'Order percentage'.dataIndex: 'cumOrdNumRatio'.align: 'right'.render: (value: number) = > renderRatioValue(value),
  },
  {
    title: 'Percentage of Order Amount'.dataIndex: 'cumOrdAmountRatio'.align: 'right'.render: (value: number) = > renderRatioValue(value),
  },
  {
    title: 'Order number'.dataIndex: 'cumOrdNum'.align: 'right'.render: (value: number) = > renderNumValue(value),
  },
  {
    title: 'Order Amount'.dataIndex: 'cumOrdAmount'.align: 'right'.render: (value: number) = > renderNumValue(value),
  },
]

export const List: React.FC = observer(() = > {
  const resData = toJS(store.distributionList.value)
  return (
    <Table
      rowKey={randomRowKey}
      columns={columns}
      dataSource={resData.thresholdDetailList}
      loading={store.distributionList.fetching}
      pagination={{ showQuickJumper: true.showSizeChanger: true }}
      bordered
      size="middle"
    />)})Copy the code

Note: In ANTD, the rowKey in the TABLE component must be unique, usually using the id returned from the back end, or randomRowKey using a public method to generate a random number, otherwise the console will report a warning

Test code:

 it('Test can request data correctly'.async() = > {const {
      unitPriceDistribution: { distributionList },
    } = store
    // 1. Mock some fake arguments
    await distributionList.fetch({ 
      body: {
        deptLevel: 2.deptId: '837'.cidLevel: 12.cid: 'test'.deptName: 'abc',}})// 2. the mock back end returns data
    runInAction(() = > { 
      distributionList.value.thresholdDetailList = [
        {
          thresholdValue: 12.thresholdName: 'nn'.ordNum: 123.ordAmount: 500.ordNumRatio: 0.12.ordAmountRatio: 0.12,}]})const app = wrap()
    const table = app.find(Table).at(0)
    // 3. Test the dataSource property of the Table component and compare it with the data you fetch from the back end
    expect(table.prop('dataSource')).toEqual(distributionList.value.thresholdDetailList) 
    distributionList.restore()
    app.unmount()
  })

  it('Test table data processing methods'.() = > {
    const app = wrap()
    const {
      unitPriceDistribution: { modalType },
    } = store
    modalType.set(3)
    // 4. The render method is written outside separately to test whether the render is correct by passing in different data
    expect(renderColValue(0, { thresholdName: '5% ~ 10% 9-95 discount' })).toBe('5% ~ 10% 9-95 discount') 
    modalType.restore()
    expect(renderRatioValue(0.123)).toBe('12.3%')
    expect(renderRatioValue(0.77)).toBe('77%')
    expect(renderNumValue(6789.1234)).toBe('6789')
    app.unmount()
  })
Copy the code

Finally, for more rigor, we can also test whether the rendered data in each cell is as expected:

it('Test table displays data correctly'.() = >{... app.update()const table = app.find(Table)
  const tr = table.find('tr').at(1)
  
  expect(
    tr
    .find('td')
    .at(1)
    .text(),
  ).toBe('100 minus 20') 
  // 5. We should use toBe to compare values and strings, and toEqual to compare objects and arrays.

  expect(
    tr
    .find('td')
    .at(2)
    .text(),
  ).toBe('high')})Copy the code

5. Test the SearchForm component

The source code:

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) = >{ 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)
Copy the code

The Form Form has a few caveats

No.1 how to make the test statement also have a form attribute?

How is the instance of the No.2 Form component obtained?

No.3 onSubmit if asynchronous request is not returned?

At this time in my heart there is a 🐴

Next, let’s answer them all

No.1 Cannot get the component instance 😲 using the form.create () modifier

You would normally use the @form.create () decorator syntax, but it might not get the Form instance properly

@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
Copy the code

Therefore, you need to change the form.create () modifier to be called functionally:

interface IProps { // Notice herestore? : {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) // The answer is here
Copy the code

If you are using TS, you also need to pass the props type definition for the current component to form.create (), otherwise a type error will be thrown. Because ANTD uses it internally for further type definition.

Similarly, the test statement looks like this:

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>.)Copy the code

The Form component has the this.props. Form property, but how do we get the component instance from wrappedComponentRef?

As mentioned on antD official website, you can get the REF by using the wrappedComponent entref provided by RC-Form

antd Form

Test statement:

let formInstance: any

const wrapper = () = >
  mount(
    <Provider store={store}>
      <SearchForm/ / look herewrappedComponentRef={(formEle: any) = > { 
          formInstance = formEle
        }}
      />
    </Provider>.)Copy the code

Using the wrappedComponentRef property, specify a callback function whose callback argument gets the current form instance.

Note: A form triggered submit method has a default callback parameter object that contains many browser native methods and properties. (such as the event)

Therefore, if your source code uses the related attribute, we must simulate it when testing, otherwise the source code will definitely throw an exception.

formInstance.submit({
  preventDefault: jest.fn(), // jest. Fn returns an empty function
})
Copy the code

Specific test statements:

it('Test submit method'.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() // what is here 🐷})})Copy the code

The code above no.3 answers our third question

Because the setFieldsValue method on the ANTD Form object is asynchronous. So, I’m going to put a setTimeout here. Otherwise, the test case may never be tested successfully.

The done method is used to solve the problem of asynchronous code testing. In an asynchronous statement, your test will be completed before the callback is called. So, use a parameter named done. Jest will wait for the callback to complete for testing.

6. Test helper. Ts

Helper files are all about the interaction methods of the page. If you are using a pure function, just pass in different data and test it.

The source code:

// There needs to be an event, a browser native property
export const filter = (form: WrappedFormUtils) = > (e: React.FormEvent<HTMLFormElement>) = > { 
  e.preventDefault() // Notice here
  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)) {
        // Delete object attributes with Reflect in ES6
        Reflect.deleteProperty(query, 'cid') 
        Reflect.deleteProperty(query, 'cidLevel')}await store.thresholdRange.fetch({ body: { ...query } })
      store.sliderScope.set(store.thresholdRange.value.minAndMaxThreshold as number[])}})}Copy the code

React’s composite event system is a subset of the native DOM event system, which only implements the DOM level3 event interface and harmonizes browser compatibility issues.

Some React events are not implemented or cannot be implemented due to certain limitations, such as the Window resize event.

Deep in the React Tech Stack

This section focuses on how to write a test statement when a form attribute is passed to a method.

Test code:

it('filter, filter amount threshold '.async() = > {const values = {
    promoDataTimeScope: 'half_year'.deptLevel: 2.deptId: '837'.cidLevel: 12.cid: ' '.deptName: 'abc',}const form = ({ 
    // In ts validateFields is mandatory, mock a false function
    validateFields: jest.fn(cb= > {
      // Emulation has no error commits
      cb(null, values)
    }),
  } as unknown) as WrappedFormUtils
  // Event also needs a mock
  const e = ({ preventDefault: jest.fn() } as unknown) as React.FormEvent<HTMLFormElement>
  // here spyOn backend request interface ❗
  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([])
})
Copy the code

Note: The above code has a sequential issue

const spy = jest.spyOn...
expect(spy).toHaveBeenCalled()
Copy the code

Jest. SpyOn must be written first or the test will fail because store.thresholdRange has not yet been requested.

// error
expected: > =1
received: 0
Copy the code

7. Other knowledge

Now that common component testing methods are covered, here are some other points to note:

  • All the above are functional component test methods, if yesclassComponent, how to do it, in fact, the principle is the same, a lot of sample code have, you only need to borrow (fu) authentication (zhi) on the next ok 😏

How are methods in a class component tested

The source code:

import { observer } from 'mobx-react'

@observer
export class BatchUpload extends React.Component<IProps> {
  // Note that public should be used instead of private
  public showBatchModal = () = > { 
    this.setState({
      visible: true})},public render() {
    const {
      form: { getFieldDecorator },
    } = this.props
    const { spinning } = this.state

    return (
      <>
        <Modal
          title="Batch import"
          onOk={this.handleOk}
          visible={this.state.visible}
          footer={this.renderFooter()}
          destroyOnClose
        >
          <Spin spinning={spinning}>
            <Form layout="horizontal">.</Form>
          </Spin>
        </Modal>
      </>)}}export default Form.create<IProps>()(BatchUpload)
Copy the code

Test code:

import { BatchUpload } from 'page/PromoRules/BatchUpload'

it('Test the showBatchModal method'.() = > {
  const app = wrapper()
  / / look here
  const instance = app.find(BatchUpload).instance() as BatchUpload 
  instance.showBatchModal()

  expect(instance.state.visible).toBeTruthy()
  app.unmount()
})
Copy the code

See, we just need find to find the component, and then instance to instantiate the class, so easy

  • In functional components,useEffectThis hook is executed when the page is initialized, so when testing, it shouldmountUnder the
const Comp: React.FC = () = > {
  React.useEffect(() = > {
    store.previewSpec.fetch()
  }, [])
  return <br />
}

it('Test renders page structure correctly'.() = > {
  runInAction(() = > {
    router.history.location.pathname = '/preview'
  })
  mount(<Comp />)
  expect(app.find(Descriptions.Item)).toHaveLength(5)})Copy the code
  • The following error is reported because there is nojest.fn, directly requesting the actual URL
// error
only absolute urls are supported
Copy the code
  • After writing a test file, you can run it--coverageCommand to view coverage of branches or statements. You can also locate a folder to view coverage of modules.
npx jest page/PromoRules/BatchUpload.test.tsx --coverage
Copy the code

Nodule 8.

The above is their own in the development process of some common test methods, however, in the actual work, many people do not like to write tests, most of the time is dot dot dot, program no problem, online 😀 behind the test out a lot of bugs, (I used to think so 😂)

Later, in the process of writing jEST tests, I was able to find bugs in my own programs and reflect on my own code logic, which was a mutually reinforcing process. Writing unit tests with high coverage and high pass rate was also a plus, whether in ensuring code quality or in future interviews.

Finally, the question of test coverage

bad:

good:

Try to make sure that all the code is covered (obsessive 😆), which is difficult in real development, including the daily development schedule, and the problem of repetitive coding.

But, you don’t feel a piece of green very cool 😎

After the

PS: If you find this article helpful, please manually like it, thanks ~