JEST

umi test --coverage
umi test --watch
umi test \allWorkbooks
// index.test.tsx
import 'jest';
import Index from '..';
import React from 'react';
import renderer, { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer';

describe('Page: index', () => {
  it('Render correctly', () => {
    const wrapper: ReactTestRenderer = renderer.create(<Index />);
    expect(wrapper.root.children.length).toBe(1);
    const outerLayer = wrapper.root.children[0] as ReactTestInstance;
    expect(outerLayer.type).toBe('div');
  });
});

react 测试利器 enzyme 有三种渲染方式:shallow, mount, render。

  • shallow 浅渲染,仅仅对当前 jsx 结构内的顶级组件进行渲染,而不对这些组件的内部子组件进行渲染,速度快
  • mount 完整渲染,渲染结果和浏览器渲染结果一样,可以对内部子组件实现复杂交互功能的组件进行测试。
  • render 完整渲染,只调用了组件的 render 方法,得到 jsx 并转码为 html,所以组件的生命周期方法内的逻辑都测试不到

without state

import React from 'react'
import SingleBireport from '../components/SingleReport/SingleBireport.tsx'
import { shallow } from 'enzyme'
import { singleReport } from './mockData'

jest.mock('react-redux', () => ({
  connect: () => jest.fn(),
  useSelector: jest.fn((fn) => fn()),
  useDispatch: () => jest.fn(),
}))

describe('Single Bireport', () => {
  const singleBireport = shallow(
    <SingleBireport workbook={singleReport} key='1' position={position} category={'category'} />
  )
  it('test Single Bireport has icon', () => {
    const icon = singleBireport.find('#titleIcon')
    expect(icon.at(0).exists()).toBe(true)
  })
})

mock provider

// MockProvider.js
import React from 'react'
import configureStore from 'redux-mock-store'
import { Provider } from 'react-redux'
import { mergeDeepRight } from 'ramda'
import { commonState, userState } from './mockData'

export const getMockProvider = (partialState) => {
  const mockStore = configureStore()
  const store = mockStore(
    mergeDeepRight(
      {
        common: commonState,
        user: userState,
      },
      partialState
    )
  )

  return {
    MockProvider: ({ children }) => {
      return <Provider store={store}>{children}</Provider>
    },
    store,
  }
}

with state

// service
import { useDispatch, useHistory, useLocation, useSelector, shallowEqual, useRouteMatch } from 'dva'
import { initialize } from '@/utils/common'
import { useEffect, useState } from 'react'

export const useConnect = (mapStateToProps, compare = shallowEqual) => {
  const dispatch = useDispatch()
  const history = useHistory()
  const location = useLocation()
  const state = useSelector(mapStateToProps, compare)
  const match = useRouteMatch()

  return {
    dispatch,
    history,
    location,
    match,
    ...state,
  }
}
import React from 'react'
import AllWorkbooks from '../allWorkbooks/allWorkbooks'
import DashboardList from '../components/DashboardList/DashboardList'
import { mount } from 'enzyme'
import { getMockProvider } from './MockProvider'
import reports from '@/models/reports'

jest.mock('react-router', () => ({
  useHistory: jest.fn().mockReturnValue({ location: { pathname: '/reports/all' } }),
  useLocation: jest.fn().mockReturnValue({ pathname: '/reports/all' }),
  useRouteMatch: () => ({ match: '' }),
}))

jest.mock('umi/link', () => 'a')

const setup = (partialState) => {
  const { MockProvider } = getMockProvider(partialState)
  return {
    MockProvider,
  }
}

describe('allworkbook list', () => {
  const { MockProvider } = setup({
    reports: reportsState,
  })

  const workbookListWrapper = mount(
    <MockProvider>
      <AllWorkbooks />
    </MockProvider>
  )

  it('test workbook list', () => {
    const dashboardLists = workbookListWrapper.find(DashboardList)
    expect(dashboardLists.length).toBe(2)
  })
})

dva reducers

import React from 'react'
import AllWorkbooks from '../allWorkbooks/allWorkbooks'
import DashboardList from '../components/DashboardList/DashboardList'
import { mount } from 'enzyme'
import { getMockProvider } from './MockProvider'
import bireport from '@/models/bireport'
import reports from '@/models/reports'
import { bireportAll, externalData, permissions, siteConfig } from './mockData'

describe('allworkbook list', () => {
  const reports_initialState = {
    externalList: {},
  }

  const reportsState = reports.reducers.saveMarket(reports_initialState, {
    payload: {
      state: 'externalList',
      data: externalData.data,
    },
  })

  it('test reports reducer', () => {
    expect(Object.keys(reportsState.externalList).length).toBe(1)
  })

  const { MockProvider } = setup({
    reports: reportsState,
  })
})

dva effects

import bireport from '@/models/bireport'
import * as usersService from '@/services'
import { call } from 'ramda'
import { userState } from './mockData'

describe('Dashboard filter', () => {
  it('test fetchAll effects', () => {
    const actionCreator = {
      type: 'bireport/fetchAll',
      payload: {
        api: 'bireportAll',
        state: 'allWorkbooks',
      },
      storage: 'allMulti',
    }
    const generator = bireport.effects.fetchAll(actionCreator, { call: call, select: jest.fn() })
    let next = generator.next()
    next = generator.next({
      user: userState,
    })
    expect(next.value).toEqual(call(usersService.getItem, actionCreator.payload))
  })
})

mock part of module

jest.mock('@/services/use.js', () => {
  const useFunction = require.requireActual('@/services/use.js')
  return {
    ...useFunction,
    useDebounce: jest.fn((fn) => fn),
  }
})

mock dispatch

import * as service from '@/proxy-components/ProxyInput/service'

const mockDispatch = jest.fn().mockResolvedValue(() => {
  const { autoCompleteData } = require.requireActual('./mockData')
  return autoCompleteData
})

jest.mock('react-redux', () => ({
  connect: () => jest.fn(),
  useSelector: jest.fn((fn) => fn()),
  useDispatch: () => mockDispatch,
}))

const fetchData = jest.spyOn(service, 'fetchCompleteData')
// Simulate input keyword
expect(fetchData).toHaveBeenCalledWith(mockDispatch, 'test')

react-dom/test-utils

import ReactDOM from 'react-dom'
import { act, Simulate } from 'react-dom/test-utils'

describe('Autocomplete for search input', () => {
  let container

  beforeEach(() => {
    container = document.createElement('div')
    document.body.appendChild(container)
  })

  afterEach(() => {
    document.body.removeChild(container)
    container = null
  })

  it('test autocomplete effects', () => {
    act(() => {
      ReactDOM.render(<ProxyInput />, container)
    })
    const searchInput = container.querySelector('input')
    expect(searchInput.value).toBe('')

    // Simulate input keyword
    act(() => {
      Simulate.focus(searchInput)
      searchInput.value = 'test'
      Simulate.change(searchInput)
    })
    expect(searchInput.value).toBe('test')
  })
})

setTimeout

it('test autocomplete effects', (done) => {
  setImmediate(() => {
    const options = container.querySelector('#options')
    expect(options).toBeDefined()
    expect(options.children.length).toBe(3)
    options.children.forEach((item) => {
      expect(item.querySelector('img')).toBeDefined()
    })
    done()
  })
})

Simulate check

const spy_fetchData = jest.spyOn(fetchDatas, 'fetchData')
let options = dashboardPage.find('input')
options.at(0).simulate('change', { target: { checked: true } })
// checked
options = dashboardPage.find('input')
expect(options.at(0).prop('checked')).toBe(true)
expect(spy_fetchData).toHaveBeenCalled()

Simulate input

let inputWrapper = items.at(0).find('Input')
inputWrapper.simulate('change', { target: { value: 'test' } })
UpdateModalWrapper.update() // 刷新根组件
items = UpdateModalWrapper.find(Item)
inputWrapper = items.at(0).find('Input')
expect(inputWrapper.props().value).toBe('test')

className

import { shallow, mount } from 'enzyme'

it('test filter icon close', () => {
  const FilterIconShallow = shallow(<FilterIcon isOpen={false} />)
  const FilterIconMount = mount(<FilterIcon isOpen={false} />)
  expect(FilterIconMount.props().isOpen).toBe(false)
  expect(FilterIconShallow.props().className).toContain('close')
})

it('test modeSwitch icon', () => {
  const modeWrapper = mount(
    <MockProvider>
      <ModeSwitch />
    </MockProvider>
  )
  // simulate open options;
  switchIcon.at(0).simulate('click')

  options = modeWrapper.find('#view')
  options.forEach((item) => {
    if (item.text() === 'Grid View') {
      expect(item.hasClass('redText')).toBe(true)
    } else if (item.text() === 'Table View') {
      expect(item.hasClass('redText')).toBe(false)
    }
  })
})

Problems in CI/CD

@@/history” is referencing to src/pages/.umi/history.js

@@/umiExports” is referencing to src/pages/.umi/umiExports.ts

Folder .umi will only be created when running “umi dev” The folder.umi will consist of the following files by default or other files such as history.js and umiExports.ts based on plugins(umi.js, router.js)

Folder .umi will not be tracked under git as it will be created every time “umi dev” is run.

Folder .umi-production will only be created when running “umi build” and will be deleted automatically when build has completed

Temporary files under .umi is important as there are functions that has dependencies on it, including test scripts.

This causes issue when we are setting up the unit test in Jenkins as there is no folder .umi for functions to import or reference

It does not make sense to run “umi dev” in Jenkins as it will increase the cost of Continuous Integration.

reference:

https://v2.umijs.org/guide/app-structure.html#src-pages-umi

https://v2.umijs.org/guide/mock-data.html

jest

Category: React

Author: Yoga

Article
Tagcloud
DVA Java Express Architecture Azure CI/CD database ML AWS ETL nest sql AntV Next Deep Learning Flutter TypeScript Angular DevTools Microsoft egg Tableau SAP Token Regexp Unit test Nginx nodeJS sails wechat Jmeter HTML2Canvas Swift Jenkins JS event GTM Algorithm Echarts React-Admin Rest React hook Flux Redux ES6 Route Component Ref AJAX Form JSX Virtual Dom Javascript CSS design pattern