[玩转 electron 系列]测试篇

前言

最近在研究 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 的工具库吧!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值