前言

最近在研究 ipc 通信方面的测试。这部分测试可分为两部分: 主进程的测试和渲染进程的测试。

前端框架为 react ,测试框架为 jest + enzyme

用到的库和版本信息有:

"react": "17.x",

"@testing-library/jest-dom": "^5.14.1",

"@testing-library/react": "^12.1.2",

"@testing-library/user-event": "^13.5.0",

"@types/enzyme": "^3.10.12",

"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",

"enzyme": "^3.11.0",

"jest": "^27.1.1",

"ts-jest": "^28.0.4",

"ts-node": "^10.8.1",

开干

jest 配置

jest.config.ts

import type {Config} from '@jest/types'

const config: Config.InitialOptions = {

moduleNameMapper: {

'@electron/(.*)': '/electron/$1',

'@src/(.*)': '/src/$1',

},

preset: 'ts-jest',

verbose: true,

testEnvironment: 'node',

moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],

testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],

setupFilesAfterEnv: ['/setup-jest.ts'],

globals: {

'ts-jest': {

tsconfig: '__tests__/tsconfig.json',

}

}

}

export default config

setup-jest.ts

import Adapter from '@wojtekmaj/enzyme-adapter-react-17'

import '@testing-library/jest-dom'

import {configure} from 'enzyme'

configure({adapter: new Adapter()})

主进程测试

主要思路就是 mock ipcMain,在正式测试之前需要配置 mock。这部分相对来说比较简单

jest.mock('electron', () => {

const mockIpcMain = {

on: jest.fn().mockReturnThis(),

handle: jest.fn().mockReturnThis(),

}

const mockIpcRenderer = {

on: jest.fn(),

send: jest.fn(),

sendSync: jest.fn(),

}

return {

ipcMain: mockIpcMain,

ipcRenderer: mockIpcRenderer,

app: {

isPackaged: false,

getName: () => {},

getVersion: () => {},

getAppPath: () => '',

},

}

}, {virtual: true})

则测试就可以正常写了

ipc.ts

import {ipcMain} from 'electron'

export const ipc = () => {

ipcMain.handle('getValue', () => {

return 'foo'

})

}

api.spec.tsx

import {ipcMain} from 'electron'

import {ipc} from './ipc.ts'

describe('electron api events', () => {

let api: any

beforeAll(() => {

ipc()

})

beforeEach(() => {

genApi()

})

it('getValue event', () => {

const getValue= api['getValue']

expect(getValue).toBeDefined()

const v = getValue('foo')

expect(v).toBe('foo')

})

function genApi() {

const mockApi = {}

const mockIpcMainOn = ipcMain.on as jest.Mock

const mockIpcMainHandle = ipcMain.handle as jest.Mock

mockIpcMainOn.mock.calls.forEach(v => {

mockApi[v[0]] = v[1]

})

mockIpcMainHandle.mock.calls.forEach(v => {

mockApi[v[0]] = v[1]

})

senderFn.mock.calls.forEach((v: string[]) => {

mockApi[v[0]] = v.slice(1)

})

api = mockApi

}

})

渲染进程测试

主要思路就是模拟 contextBridge.exposeInMainWorld,所以需要一个方法将所有通信都绑到 window 上。

utils.ts

export const setupForTest = () => {

const entries = Object.keys(api).map(key => [key, jest.fn()])

const mockElectronApi = Object.fromEntries(entries) as MockApi

window[electronApi] = mockElectronApi

return mockElectronApi

}

export const waitForComponentToPaint = async (wrapper: ReactWrapper) => {

await act(async () => {

await new Promise(resolve => setTimeout(resolve))

wrapper.update()

})

}

说明: setupForTest 用于让 window 有api, 其中,api 就是定义接口的地方,即在正常 preload.ts 中传给 contextBridge.exposeInMainWorld 的第二个参数。 waitForComponentToPaint 是为了解决在 function component 中,若执行了某个函数会有副作用,则会有警告 When testing, code that causes React state updates should be wrapped into act(...):

然后就可以开始写测试了

index.tsx

export const MyComponent: FC<{}> = () => {

const {electronApi} = window

const [text, setText] = useState('')

const setTest1 = async () => {

const t = await electronApi.getText()

setText(t)

}

return (

{text}

)

}

com.spec.tsx

/**

* @jest-environment jsdom

*/

import {MyComponent} from './index.tsx'

import {mount, ReactWrapper} from 'enzyme'

import {waitForComponentToPaint, setupForTest} from './utils'

describe('electron api events', () => {

let wrapper: ReactWrapper

let mockApi: any

beforeEach(() => {

mockApi = setupForTest()

wrapper = mount()

})

afterEach(() => {

wrapper.unmount()

})

it('getHistoryPrinter event', async () => {

mockApi.getText.mockResolvedValue('foo')

const btn = wrapper.find('button').at(0)

expect(btn.text()).toBe('test1')

btn.invoke('onClick')!({} as any)

await waitForComponentToPaint(wrapper)

expect(mockApi.getText).toHaveBeenCalled()

const text = wrapper.find('[data-testid="example"]')

expect(text.text()).toBe('foo')

})

})

其中: btn.invoke('onClick')!({} as any) 这行可能比较奇怪,但我也不知道为什么,感叹号也不掉,最后一个括号里面我 new 一个 MouseEvent(不管是用lib的还是react的)也不行,所以只能写这种奇怪的代码了。

问题记录

除了以上的几个问题及解决办法,其他我遇到的问题也贴出来供和大伙讨论。

问题描述并未将所有报错全贴出来,仅将关键部分贴了出来。

问题描述 \node_modules\jest-environment-jsdom\build\index.js" does not export a "getVmContext" method, which is mandatory from Jest 27. This method is a replacement for "runScript".

解决:

package.json 加上

“resolutions”: { “jest-environment-jsdom”: “27.4.6” },

问题描述 TypeError: Cannot read properties of undefined (reading 'html')

at new JSDOMEnvironment (node_modules/jest-environment-jsdom/build/index.js:72:44)

解决:

将 jest 的版本降到27

问题描述 ...at getFiber (node_modules/enzyme-adapter-react-16/src/detectFiberTags.js:15:35)...

解决:

adapter 使用 @wojtekmaj/enzyme-adapter-react-17

小结

有几个库也有做 mock electron ipc 的事情,但是都和我的实际需求有出入,所以最终还是决定将所有的工具函数自己实现了一遍。

希望官方尽快出 react17 的 adapter 吧! 希望官方尽快出方便测 function component 的工具库吧!

推荐阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: