Pre-technical reserve

preface


I am testing xiaobai, xiaobai, xiaobai. Recently, I wanted to introduce testing into a certain size project, so I found some materials to learn, and now I have successfully introduced it into the project. Therefore, I want to summarize the learning path and difficulties, pits and attention points encountered in writing tests when thinking clearly and remembering deeply. To make a summary of their recent learning achievements, but also hope to help people like me who just entered the test.

Attention attention special attention!!


React Native Can run jest tests with 0.56.0, 0.56 regression, and Can’t run jest tests with 0.56.0. Jest. Mock only works when defined in jestsetup. js, not in individual Snapshots tests. “Can’t run jest tests with 0.56.0” fixed in 0.57, “0.56 Regression: Jest. Mock only works when defined in jestsetup. js, not in individual Snapshots tests “– this issue still exists in 0.57. Therefore, the article sample is recommended to run in version 0.55.4.

Important concepts to understand in the initial test


  • Automated testing
  • Test the Pyramids
  • Unit/integration/E2E testing

Read more: How to Automate Test React Native Projects (Part 1) – Learn about the concepts above with E2E automation.

As the project grows larger, the additional requirements may not be too much work for development, but for testing, especially regression testing, the stress increases significantly, and it is not stable testing if it is done manually or if some test cases are dropped. Therefore, the importance of automated testing is reflected. The general idea of automated testing is “test pyramid”, which is E2E testing, integration testing and unit testing from top to bottom. E2E test is need real compile packaging on the simulator or on simulated user behavior test process, test results by the network, the pop-up window, telephone calls and such uncontrollable influence is bigger, so you can’t trust too much, so the E2E testing out the bugs can best in integration testing, integration testing of the Bug is best can reproduce in unit testing, If not, more unit/integration tests should be added to reproduce the Bug. Integration and unit tests do not need to be compiled and packaged to run, so their execution is very fast, so the amount of code tested in the project should be more unit tests than integration tests, and integration tests than E2E tests, forming an automated test pyramid.

  • Snapshot
  • Mock
  • JavaScript Testing utility: for example, Detox, Enzyme
  • JavaScript Test runners and assertion libraries: such as Jest

These concepts will be explained later in the article.

React Native support for testing


  • Github. IO /react-nativ…

If you’re interested in testing a React Native app, check out the React Native Tutorial on the Jest website.

  • Jestjs. IO/docs/en/tut…

Starting from react-native version 0.38, a Jest setup is included by default when running react-native init.

React-native 0.38 and later versions of React-Native init already have the Jest test library installed by default, so we can start writing test code with 0 configuration.

To get started, create a __test__ folder in the ios and Android directories and create a helloworld.test.js file in the __test__ folder and type the following code:

it('test',() = > {expect(42).toequal (42)})Copy the code

Run NPM test on the terminal to view the test result. Getting started is super easy o(* ̄ *)o!

Note: It is not necessary to create a __test__ folder in the ios and Android directories to write the test code; *.test.js under the project can execute the tests.

Essential knowledge of Jest


  • Expect
  • Snapshot Testing
  • Mock Functions

Please read the jestjs. IO/docs/en/get… The first 5 articles in the Introduction chapter (up to Mock Function) and the first article in the Guides chapter.

Jest is a Test Runner and assertion library that uses Expect to assert whether the current and expected results are the same, the types of data involved here. Jest uses mocks to simulate functions, modules, and classes for easy testing (Mock tests that don’t need to actually execute code, such as Fetch, platform. OS, etc.).

In the previous UI test, the test script was executed and the screenshots were taken on the page that stayed. When the same test script was executed again, the screenshots before and after would be compared. If the pixels were the same, the test passed; if the pixels were different, the test failed. The React UI test in Jest can generate a serialized structure tree (in text form) with Snapshot. Snapshot can be used not only to test uIs, but to test any structure that can be serialized, such as Action, Store, etc., as discussed later in this article.

With the initial technology in place, we can start writing tests

Unit testing

Redux logic tests


React Native with the New Jest — Part II

Reducer tests in Redux

Reducer is a pure function, that is, if there is the same input value, there must be the same output, so it is easy to test.

it('start upload action will combine upload\'s watting queue and failed queue then update upload\'s uploading state', () = > {let currentState = Map({
        'uploadTestKey': new Upload({
            name: 'uploadTestKey',
            wattingQueue: List([
                new UploadItem({
                    name: 'fileTwo',
                    filepath: 'fileTwoPath'
                })
            ]),
            uploadedQueue: List([
                new UploadItem({
                    name: 'fileThree',
                    filepath: 'fileThreePath'
                }),
            ]),
            failedQueue: List([
                new UploadItem({
                    name: 'fileOne',
                    filepath: 'fileOnePath'
                }),
            ]),
        })
    })
    currentState = UploadReducer(currentState, UPloadActions.startUpload({upload: 'uploadTestKey'}))
    expect(currentState).toMatchSnapshot()
})
Copy the code

The above code sample is test UploadReducer currentState of fixed input and UPloadActions startUpload ({upload: ‘uploadTestKey}) output is correct, here need to pay attention to the following two points:

1. Make sure the __snapshots__/< test file name >. Snap generated after the first run of NPM run test is correct. Because expect(currentState).tomatchSnapshot () is written differently from expect(Value).toEqual(someValue), which gives the expected value directly when writing test cases, After the test case is run, the expected value is automatically written to the __snapshots__/< test file name >.snap file. Therefore, after the first run of the test case, we need to verify the generated snapshot is correct. The benefit of toMatchSnapshot() is that no copy code is required in the test case. Without toMatchSnapshot(), our test case would be written in the following form:

it('start upload action will combine upload\'s watting queue and failed queue then update upload\'s uploading state', () = > {let currentState = Map({
        'uploadTestKey': new Upload({
            name: 'uploadTestKey',
            wattingQueue: List([
                new UploadItem({
                    name: 'fileTwo',
                    filepath: 'fileTwoPath'
                })
            ]),
            uploadedQueue: List([
                new UploadItem({
                    name: 'fileThree',
                    filepath: 'fileThreePath'
                }),
            ]),
            failedQueue: List([
                new UploadItem({
                    name: 'fileOne',
                    filepath: 'fileOnePath'
                }),
            ]),
        })
    })
    currentState = UploadReducer(currentState, UPloadActions.startUpload({upload: 'uploadTestKey'}))
    expect(currentState.is(
        Map({
        'uploadTestKey': new Upload({
            name: 'uploadTestKey',
            wattingQueue: List([
                new UploadItem({
                    name: 'fileTwo',
                    filepath: 'fileTwoPath'
                }),
                new UploadItem({
                    name: 'fileOne',
                    filepath: 'fileOnePath'
                }),
            ]),
            uploadedQueue: List([
                new UploadItem({
                    name: 'fileThree',
                    filepath: 'fileThreePath'
                }),
            ]),
            failedQueue: List([]),
        })
    })
    )).toBe(true)})Copy the code

This creates code redundancy and this is where the importance of snapshot comes into play.

2. Since it is a unit test, the responsibility of each test case we write should be single. Do not write the integration test in the unit test, which is often difficult to distinguish between just learning the test. The syntax of a test is not difficult, but what test cases to write. For example, the above test case is to test an upload queue component. Its Reducer can handle multiple actions, such as push, delete, upload, etc. Then how should we write unit tests for this Reducer? The author went off the rails at the beginning and wrote a test case like this, which you can see:

describe("upload component reducer test", () => {
    describe("one file upload", () = > {let currentState = Map({})
        beforeAll(() => {
            currentState = UploadReducer(currentState, UPloadActions.registerUpload({upload: 'uploadTestKey'}))
            expect(currentState).toMatchSnapshot()
        })
    
        afterAll(() => {
            currentState = UploadReducer(currentState, UPloadActions.destroyUpload({upload: 'uploadTestKey'}))
            expect(currentState).toMatchSnapshot()
        })
        ...
        test("handle upload success", () = > {let state = UploadReducer(currentState, UPloadActions.pushUploadItem({upload: 'uploadTestKey', name: 'fileOne', filePath: 'fileOnePath'}))
            expect(state).toMatchSnapshot()
            state = UploadReducer(state, UPloadActions.startUpload({upload: 'uploadTestKey'}))
            expect(state).toMatchSnapshot()
            state = UploadReducer(state, UPloadActions.startuploadItem({upload: 'uploadTestKey'}))
            expect(state).toMatchSnapshot()
            state = UploadReducer(state, UPloadActions.uploadItemSuccess({upload: 'uploadTestKey', id: '12345'}))
            expect(state).toMatchSnapshot()
            state = UploadReducer(state, UPloadActions.uploadComplete({upload: 'uploadTestKey'}))
            expect(state).toMatchSnapshot()
        })

        test("handler upload failed", () => {... })test("handler reupload success", () = > {let state = UploadReducer(currentState, UPloadActions.pushUploadItem({upload: 'uploadTestKey', name: 'fileOne', filePath: 'fileOnePath'}))
            state = UploadReducer(state, UPloadActions.startUpload({upload: 'uploadTestKey'}))
            state = UploadReducer(state, UPloadActions.startuploadItem({upload: 'uploadTestKey'}))
            state = UploadReducer(state, UPloadActions.uploadItemFailed({upload: 'uploadTestKey'}))
            state = UploadReducer(state, UPloadActions.uploadComplete({upload: 'uploadTestKey'}))
            expect(state).toMatchSnapshot()
            state = UploadReducer(state, UPloadActions.startUpload({upload: 'uploadTestKey'}))
            expect(state).toMatchSnapshot()
            state = UploadReducer(state, UPloadActions.startuploadItem({upload: 'uploadTestKey'}))
            state = UploadReducer(state, UPloadActions.uploadItemSuccess({upload: 'uploadTestKey', id: '12345'}))
            state = UploadReducer(state, UPloadActions.uploadComplete({upload: 'uploadTestKey'}))
            expect(state).toMatchSnapshot()
        })
    })
    describe("mult file upload", () = > {letcurrentState = Map({}) beforeAll(() => { ... }) afterAll(() => { ... })...test("handle upload successed", () => {... })test("handle upload failed", () => {... })test("hanlde reupload successed", () => {... })})})Copy the code

Can you look at the unit test questions above? Here’s an example from this article:

reducer
action

describe("upload component reducer test", () => {
    it('register upload action will register a upload queue to state', () = > {let currentState = Map({})
        currentState = UploadReducer(currentState, UPloadActions.registerUpload({upload: 'uploadTestKey'}))
        expect(currentState).toMatchSnapshot()
    })

    it('destroy upload action will remove upload queue from state', () = > {let currentState = Map({
            'uploadTestKey': new Upload({
                name: 'uploadTestKey'
            })
        })
        currentState = UploadReducer(currentState, UPloadActions.destroyUpload({upload: 'uploadTestKey'}))
        expect(currentState).toMatchSnapshot()
    })

    it('push upload item action will add an uploadItem into upload\'s wattingQueue', () => {... }) it('delete upload item action will remove an uploadItem from upload\'s all queue', () => {... })... })Copy the code

Reducer has as many test cases as the number of actions it can handle. Isn’t that much clearer? The sample code

Action Creator test in Redux

Same as Reducer, two points should be paid attention to. First, the responsibilities of test cases should be correct, and it must be remembered that it is a “unit test”. We only need to ensure that a single Action Creator has a specific input and a specific output, and check the output snapshot of the first test case. Make sure your expected value is correct. The sample code

How do I test asynchronous Actions

A normal Action is an Object with a Type attribute, but an asynchronous Action returns a special Function instead of an Object, requiring middleware like Redux-Thunk to handle it. Therefore, we need two Mock modules when testing asynchronous actions, one is the fetch required by network asynchronism, and the other is the Store that can distribute Async actions.

Please read Jest’s official Mock documentation :Mock Functions, manual-Mocks

Mock fetch can be used with the library: jest-fetch- Mock Store Can be used with the library: redux-mock-store For specific configuration see the official README, which is the configured project. Object Action test

it('register upload action' , () => {
  store.dispatch(UploadActions.registerUpload({upload: 'uploadKey'}))
  expect(store.getActions()).toMatchSnapshot()
})
Copy the code

Asynchronous Action test:

it('upload one file fail action test', () => {
  fetch.mockResponseOnce(JSON.stringify({ error: new Error('fail')}))return store.dispatch(UploadActions.upload('uploadKey', config))
          .then(() => {
            expect(store.getActions()).toMatchSnapshot()
          })
})
Copy the code

Asynchronous tests can be written in multiple ways to handle callBack, Promise, async/await. Please refer to the official documentation for details.

Component test


Unit testing for Redux was described in detail above, so let’s see how Component does it.

Test React Native with the New Jest — Part I

Note that there are many articles on the web that use the React-native Mock library for component tests, but as of version RN0.37, the Jest setup built into The React-Native library comes with some mocks that apply to the React-Native library. This is available in setup.js, so there is no need to introduce react-native mock.

The core of the Component test:

  • For different propsIt will be differentDomThe output.
  • useActively execute instance methodsTo simulate theStateThe output variesDom.
  • Test usingconnect(component)Mocks when wrapping componentsconnectcomponent-connectedprops.Direct test byconnectPackage components.
  • Test usingHOCWhen the component ofThe test respectivelyComponentWrapandComponent.

Note the bold text in the list above, which is where we start writing Component tests.

UI Render tests, we test differentlypropsThere are differentDom:

it('render login screen with init state', () => {
    const loginWrap = shallow(
        <LoginScreen
            handleSubmit={handleSubmit}
            valid={false}
            submitting={false}
        />
    )
    expect(toJson(loginWrap)).toMatchSnapshot()
})
Copy the code

In the previous code, we could change the valid values and use toMatchSnapshot to preserve snap. Libraries are concerned in this: enzyme, enzyme – to – json, knowledge points are: shallow.

Enzyme is a test tool written in javascript language for React. It can be used to quickly get the output of Component (Dom), manipulate the Dom, and write various assertions on the Dom. React Test Utilities can also export the Dom, but it can’t manipulate the Dom. It doesn’t provide Selector. The react-testing-library is similar to the enzyme function, but does not support react-native, which supports react.

Enzyme -to-json output shallow result json-normally used with Jest’s toMatchSnapshot. Shallow renders only one layer of the Dom tree, for example:

//ComponentA.js
import React from 'react'
import {
    Text,
    View,
} from 'react-native'

class ComponentA extends React.Component {
    render() {
        return (
            <View><ComponentB /></View>
        )
    }
}
class ComponentB extends React.Component {
    render() {
        return (
            <Text>Hello world</Text>
        )
    }
}

export default ComponentA
Copy the code
//ComponentA.test.js
import ComponentA from './ComponentA'
import React from 'react'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'

it('shallow ComponentA', () => {
    const wrap = shallow(<ComponentA/>)
    expect(toJson(wrap)).toMatchSnapshot()
})
Copy the code
//ComponentA.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`shallow ComponentA 1`] = `
<Component>
  <ComponentB />
</Component>
`;
Copy the code

Shallow rendering results in


, which no longer extends ComponentB to get


Hello world

. So we don’t have to worry about the behavior of the child components, we just have to focus on testing ComponentA.

For details about how to install an enzyme and an enzyme-to-json file, see airbnb. IO /enzyme/

UI interaction tests, we need to actively invoke instance methods to triggerstateThe changes:

//Foo.js
import React from 'react'
import {
    Switch
} from 'react-native'

export default class extends React.Component {
    constructor() { super(... arguments) this.state = { value:false
        }
    }

    _onChange = (value) => {
        this.setState({value: value})
    }

    render() {
        return (
            <Switch onValueChange={this._onChange} value={this.state.value}/>
        )
    }
}
Copy the code
//Foo.test.js
import Foo from './Foo'

import React from 'react'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'

it('Foo change state', () => {
    const wrap = shallow(<Foo/>)
    expect(wrap.state(['value'])).toEqual(false)
    expect(toJson(wrap)).toMatchSnapshot()

    const firstWrap = wrap.first()
    firstWrap.props().onValueChange(true)
    expect(wrap.state(['value'])).toEqual(true)
    expect(toJson(wrap)).toMatchSnapshot()
})
Copy the code
//Foo.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Foo change state 1`] = `
<Switch
  disabled={false}
  onValueChange={[Function]}
  value={false} / > `; exports[`Foo change state 2`] = ` <Switch disabled={false}
  onValueChange={[Function]}
  value={true} / > `;Copy the code

In this example, we print snap before firstwrap.props ().onValuechange (true) and assert the value of state.value to test the state change caused by onValueChange. Firstwrap.props ().onValuechange (true) is the action of actively calling the instance method.

HOC test:

In the two examples above, you can understand unit testing for regular components, but how to test Hoc components? We can test Higher Order and Component separately. Component tests are the same as in the previous two examples. Note that Higher Order, Component and HOC should be exported separately:

//Hoc.js
import React from 'react'
import {
    View
} from 'react-native'

export function fetchAble(WrappedComponent) {
    return class extends React.Component{
        _fetchData = () => {
            console.log('start fetch')}render() {
            return (
                <WrappedComponent fetchData={this._fetchData}/>
            )
        }
    }
}

export class Com extends React.Component {
    render() {
        return (<ComponentA/>)
    }
}

export default fetchAble(View)
Copy the code
//Hoc.test.js
import {fetchAble} from './Hoc'
it('Hoc test'. () => { const A = (props) => <View/> const B = fetchAble(A) const fetchWarp = shallow(<B/>) const wrapA = fetchWarp.find(A) expect(wrapA).not.toBeUndefined() expect(wrapA.props().fetchData).not.toBeUndefined() wrapA.props().fetchData() expect(console.log.mock.calls.length).toEqual(1) expect(console.log.mock.calls[0][0]).toEqual('start fetch')})Copy the code

Mockconsole is configured in setupJest.

Redux Connect works the same way as HOC

Reference articles for component testing (taking ladders) :

Sharing and Testing Code in React with Higher Order Components

Testing the React Component ‘s State

Unit Testing Redux Connected Components

This article focuses on unit testing around components and Redux, and the next article will start writing about integration and E2E testing

Welcome to my Jane books home page: www.jianshu.com/u/b92ab7b3a… Articles updated simultaneously ^_^