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 yes
class
Component, 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,
useEffect
This hook is executed when the page is initialized, so when testing, it shouldmount
Under 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 no
jest.fn
, directly requesting the actual URL
// error
only absolute urls are supported
Copy the code
- After writing a test file, you can run it
--coverage
Command 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 ~