前言
最近在研究 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/(.*)': '<rootDir>/electron/$1',
'@src/(.*)': '<rootDir>/src/$1',
},
preset: 'ts-jest',
verbose: true,
testEnvironment: 'node',
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
setupFilesAfterEnv: ['<rootDir>/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 (
<div>
<button onClick={setTest1}>test1</button>
<div data-testid="example">{text}</div>
</div>
)
}
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(<MyComponent></MyComponent>)
})
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 的工具库吧!