|
@@ -1,12 +1,12 @@
|
|
|
# Taro小程序测试规范
|
|
# Taro小程序测试规范
|
|
|
|
|
|
|
|
-版本:4.x
|
|
|
|
|
|
|
+版本:4.x (已更新为实际使用的测试框架)
|
|
|
|
|
|
|
|
## 概述
|
|
## 概述
|
|
|
|
|
|
|
|
本文档定义了出行服务项目中Taro小程序的测试规范。Taro测试使用独立的测试体系,与主项目的Vitest测试框架分离。
|
|
本文档定义了出行服务项目中Taro小程序的测试规范。Taro测试使用独立的测试体系,与主项目的Vitest测试框架分离。
|
|
|
|
|
|
|
|
-**测试框架**: Jest + @tarojs/test-utils-react
|
|
|
|
|
|
|
+**测试框架**: Jest + @testing-library/react + 自定义Taro组件mock
|
|
|
**测试位置**: `mini/tests/` 目录
|
|
**测试位置**: `mini/tests/` 目录
|
|
|
**测试范围**: 组件测试、页面测试、应用级测试、多端测试
|
|
**测试范围**: 组件测试、页面测试、应用级测试、多端测试
|
|
|
|
|
|
|
@@ -18,13 +18,12 @@
|
|
|
# 进入mini目录
|
|
# 进入mini目录
|
|
|
cd mini
|
|
cd mini
|
|
|
|
|
|
|
|
-# React:
|
|
|
|
|
-npm i @tarojs/test-utils-react --save
|
|
|
|
|
-# 安装Jest
|
|
|
|
|
-npm i jest --save
|
|
|
|
|
|
|
+# 安装测试框架
|
|
|
|
|
+npm i jest @testing-library/react @testing-library/jest-dom --save-dev
|
|
|
|
|
+npm i @types/jest jest-environment-jsdom ts-jest --save-dev
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-**注意**: @tarojs/plugin-platform-h5为前置 peerDependencies,请勿删除该依赖申明
|
|
|
|
|
|
|
+**注意**: Taro 4 项目使用 Jest + @testing-library/react 进行测试,无需安装 @tarojs/test-utils-react
|
|
|
|
|
|
|
|
## 配置
|
|
## 配置
|
|
|
|
|
|
|
@@ -33,21 +32,42 @@ npm i jest --save
|
|
|
|
|
|
|
|
```javascript
|
|
```javascript
|
|
|
// mini/jest.config.js
|
|
// mini/jest.config.js
|
|
|
-const defineJestConfig = require('@tarojs/test-utils-react/dist/jest.js').default
|
|
|
|
|
-
|
|
|
|
|
-module.exports = defineJestConfig({
|
|
|
|
|
|
|
+module.exports = {
|
|
|
|
|
+ preset: 'ts-jest',
|
|
|
testEnvironment: 'jsdom',
|
|
testEnvironment: 'jsdom',
|
|
|
- testMatch: ['<rootDir>/tests/**/*.test.{js,ts,tsx}'],
|
|
|
|
|
|
|
+ setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
|
|
|
|
+ moduleNameMapping: {
|
|
|
|
|
+ '^@/(.*)$': '<rootDir>/src/$1',
|
|
|
|
|
+ '\.(css|less|scss|sass)$': 'identity-obj-proxy',
|
|
|
|
|
+ '\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
|
|
|
|
|
+ '<rootDir>/tests/__mocks__/fileMock.js'
|
|
|
|
|
+ },
|
|
|
|
|
+ testMatch: [
|
|
|
|
|
+ '<rootDir>/tests/**/*.spec.{ts,tsx}',
|
|
|
|
|
+ '<rootDir>/tests/**/*.test.{ts,tsx}'
|
|
|
|
|
+ ],
|
|
|
collectCoverageFrom: [
|
|
collectCoverageFrom: [
|
|
|
'src/**/*.{ts,tsx}',
|
|
'src/**/*.{ts,tsx}',
|
|
|
'!src/**/*.d.ts',
|
|
'!src/**/*.d.ts',
|
|
|
- '!src/app.tsx',
|
|
|
|
|
- '!src/app.config.ts'
|
|
|
|
|
|
|
+ '!src/**/index.{ts,tsx}',
|
|
|
|
|
+ '!src/**/*.stories.{ts,tsx}'
|
|
|
],
|
|
],
|
|
|
coverageDirectory: 'coverage',
|
|
coverageDirectory: 'coverage',
|
|
|
coverageReporters: ['text', 'lcov', 'html'],
|
|
coverageReporters: ['text', 'lcov', 'html'],
|
|
|
- setupFilesAfterEnv: ['<rootDir>/tests/setup.ts']
|
|
|
|
|
-})
|
|
|
|
|
|
|
+ testPathIgnorePatterns: [
|
|
|
|
|
+ '/node_modules/',
|
|
|
|
|
+ '/dist/',
|
|
|
|
|
+ '/coverage/'
|
|
|
|
|
+ ],
|
|
|
|
|
+ transform: {
|
|
|
|
|
+ '^.+\\.(ts|tsx)$': 'babel-jest',
|
|
|
|
|
+ '^.+\\.(js|jsx)$': 'babel-jest'
|
|
|
|
|
+ },
|
|
|
|
|
+ transformIgnorePatterns: [
|
|
|
|
|
+ '/node_modules/(?!(swiper|@tarojs)/)'
|
|
|
|
|
+ ],
|
|
|
|
|
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json']
|
|
|
|
|
+}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
### package.json脚本配置
|
|
### package.json脚本配置
|
|
@@ -64,102 +84,117 @@ module.exports = defineJestConfig({
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-`defineJestConfig` 已内置了部分初始化配置,需要修改可自行配置覆盖。配置文件参考 [Jest官网](https://jestjs.io/docs/configuration)。
|
|
|
|
|
-
|
|
|
|
|
-编写测试用例
|
|
|
|
|
-组件级别
|
|
|
|
|
-// __test__/main/index.test.js
|
|
|
|
|
-import TestUtils from '@tarojs/test-utils-react'
|
|
|
|
|
-import Hello from '../../src/components/Hello.tsx'
|
|
|
|
|
-const testUtils = new TestUtils()
|
|
|
|
|
-describe('App', () => {
|
|
|
|
|
- it('RenderComponent', async () => {
|
|
|
|
|
- // React跟Vue相同用法
|
|
|
|
|
- await testUtils.mount(Hello, {
|
|
|
|
|
- props: {
|
|
|
|
|
- // 配置项
|
|
|
|
|
- a: 1,
|
|
|
|
|
- },
|
|
|
|
|
- })
|
|
|
|
|
- // 等待页面出现.btn这个节点
|
|
|
|
|
- const btn = await testUtils.queries.waitForQuerySelector('.btn')
|
|
|
|
|
- // 等待react的渲染更新完成
|
|
|
|
|
- await testUtils.act(() => {
|
|
|
|
|
- // 触发点击事件
|
|
|
|
|
- testUtils.fireEvent.click(btn)
|
|
|
|
|
- })
|
|
|
|
|
- // 打印渲染结果
|
|
|
|
|
- expect(testUtils.html()).toMatchSnapshot()
|
|
|
|
|
- // <div class="hello">...
|
|
|
|
|
|
|
+配置文件参考 [Jest官网](https://jestjs.io/docs/configuration)。
|
|
|
|
|
+
|
|
|
|
|
+## 编写测试用例
|
|
|
|
|
+
|
|
|
|
|
+### 组件级别测试
|
|
|
|
|
+```typescript
|
|
|
|
|
+// tests/components/button.test.tsx
|
|
|
|
|
+import { render, screen, fireEvent } from '@testing-library/react'
|
|
|
|
|
+import Button from '../../src/components/Button'
|
|
|
|
|
+
|
|
|
|
|
+describe('Button Component', () => {
|
|
|
|
|
+ it('应该渲染按钮并响应点击事件', () => {
|
|
|
|
|
+ const handleClick = jest.fn()
|
|
|
|
|
+
|
|
|
|
|
+ render(<Button onClick={handleClick}>点击我</Button>)
|
|
|
|
|
+
|
|
|
|
|
+ const button = screen.getByRole('button', { name: '点击我' })
|
|
|
|
|
+ expect(button).toBeInTheDocument()
|
|
|
|
|
+
|
|
|
|
|
+ fireEvent.click(button)
|
|
|
|
|
+ expect(handleClick).toHaveBeenCalledTimes(1)
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
-运行测试
|
|
|
|
|
-# package.json
|
|
|
|
|
-# scripts: {
|
|
|
|
|
-# "test": "jest",
|
|
|
|
|
-# }
|
|
|
|
|
-npm run test
|
|
|
|
|
|
|
+### 页面级别测试
|
|
|
|
|
+```typescript
|
|
|
|
|
+// tests/pages/index.test.tsx
|
|
|
|
|
+import { render, screen } from '@testing-library/react'
|
|
|
|
|
+import Index from '../../src/pages/index'
|
|
|
|
|
|
|
|
-应用级别
|
|
|
|
|
-// __test__/main/index.test.js
|
|
|
|
|
-import TestUtils from '@tarojs/test-utils-react'
|
|
|
|
|
-import Taro from '@tarojs/taro'
|
|
|
|
|
-import App from '../../src/app.ts'
|
|
|
|
|
-const testUtils = new TestUtils()
|
|
|
|
|
-describe('App', () => {
|
|
|
|
|
- it('RenderApp', async () => {
|
|
|
|
|
- await testUtils.createApp()
|
|
|
|
|
- // 监听/pages/index/index这个页面路由的onShow生命周期触发
|
|
|
|
|
- await testUtils.PageLifecycle.onShow('/pages/index/index')
|
|
|
|
|
- // 跳转到第二个页面
|
|
|
|
|
- Taro.navigateTo({ url: '/pages/second/index' })
|
|
|
|
|
- // 监听/pages/second/index这个页面路由的onShow生命周期触发
|
|
|
|
|
- await testUtils.PageLifecycle.onShow('/pages/second/index')
|
|
|
|
|
- // 当/pages/second/index触发onShow后,打印页面内容
|
|
|
|
|
- expect(testUtils.html()).toMatchSnapshot()
|
|
|
|
|
- // <body><div class="taro_router" id="app">...
|
|
|
|
|
|
|
+describe('Index Page', () => {
|
|
|
|
|
+ it('应该渲染首页内容', () => {
|
|
|
|
|
+ render(<Index />)
|
|
|
|
|
+
|
|
|
|
|
+ expect(screen.getByText('欢迎使用出行服务')).toBeInTheDocument()
|
|
|
|
|
+ expect(screen.getByRole('button', { name: '开始使用' })).toBeInTheDocument()
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 运行测试
|
|
|
|
|
+```bash
|
|
|
|
|
+# 在mini目录下执行
|
|
|
|
|
+npm run test
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 多端测试
|
|
|
|
|
|
|
|
-多端
|
|
|
|
|
对于小程序的测试,我们提供了环境变量对 Jest 环境进行区分:TARO_ENV_JEST,我们可以在调用 jest 测试时设置对应的环境变量
|
|
对于小程序的测试,我们提供了环境变量对 Jest 环境进行区分:TARO_ENV_JEST,我们可以在调用 jest 测试时设置对应的环境变量
|
|
|
|
|
|
|
|
|
|
+```json
|
|
|
// package.json
|
|
// package.json
|
|
|
-scripts: {
|
|
|
|
|
- "test": "jest",
|
|
|
|
|
- "test:weapp": "export TARO_ENV_JEST=weapp && jest"
|
|
|
|
|
|
|
+{
|
|
|
|
|
+ "scripts": {
|
|
|
|
|
+ "test": "jest",
|
|
|
|
|
+ "test:h5": "export TARO_ENV_JEST=h5 && jest",
|
|
|
|
|
+ "test:weapp": "export TARO_ENV_JEST=weapp && jest"
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
这里会有几点差异:
|
|
这里会有几点差异:
|
|
|
|
|
|
|
|
-环境差异
|
|
|
|
|
-这里会将运行代码中的process.env.TARO_ENV切换为TARO_ENV_JEST的值,主要用于一些"环境判断"比如下面的代码将会在test:weapp执行:
|
|
|
|
|
|
|
+### 环境差异
|
|
|
|
|
+这里会将运行代码中的 `process.env.TARO_ENV` 切换为 `TARO_ENV_JEST` 的值,主要用于一些"环境判断",比如下面的代码将会在 `test:weapp` 执行:
|
|
|
|
|
|
|
|
|
|
+```typescript
|
|
|
if (process.env.TARO_ENV === 'weapp') {
|
|
if (process.env.TARO_ENV === 'weapp') {
|
|
|
// ....setState(....)
|
|
// ....setState(....)
|
|
|
console.log('this is weapp')
|
|
console.log('this is weapp')
|
|
|
}
|
|
}
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
-标签差异
|
|
|
|
|
-除此之外html()输出的节点内容也会改变,将以小程序标签的形式输出,例如:
|
|
|
|
|
|
|
+### 标签差异
|
|
|
|
|
+Taro 组件会根据环境变量渲染不同的标签,例如:
|
|
|
|
|
|
|
|
-<!-- h5 -->
|
|
|
|
|
-<taro-core-view className="cls">
|
|
|
|
|
- <taro-image-view src="xxx">
|
|
|
|
|
- <img src="xxx" />
|
|
|
|
|
- </taro-image-view>
|
|
|
|
|
-</taro-core-view>
|
|
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 在 H5 环境中
|
|
|
|
|
+<View className="cls">
|
|
|
|
|
+ <Image src="xxx" />
|
|
|
|
|
+</View>
|
|
|
|
|
|
|
|
-<!-- weapp -->
|
|
|
|
|
-<view className="cls">
|
|
|
|
|
|
|
+// 在微信小程序环境中会渲染为
|
|
|
|
|
+<view class="cls">
|
|
|
<image src="xxx" />
|
|
<image src="xxx" />
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
-因为这是一个在线运行的测试工具,主要目的也是对用户行为和外部表现进行测试断言,在测试环境使用的还是以 h5 的形式来模拟整个程序的运行,对于其他小程序特有的生命周期和 API,我们推荐以下做法:
|
|
|
|
|
|
|
+### 模拟小程序 API
|
|
|
|
|
+对于小程序特有的生命周期和 API,我们推荐以下做法:
|
|
|
|
|
|
|
|
-生命周期: 使用PageLifecycle的triggerXxxxx进行生命周期的触发
|
|
|
|
|
-API: 使用jest提供的mock方法来模拟@tarojs/taro的 api,模拟小程序的返回值
|
|
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 模拟 Taro API
|
|
|
|
|
+jest.mock('@tarojs/taro', () => ({
|
|
|
|
|
+ navigateTo: jest.fn(),
|
|
|
|
|
+ request: jest.fn(),
|
|
|
|
|
+ getStorage: jest.fn(),
|
|
|
|
|
+ setStorage: jest.fn(),
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
|
|
+// 在测试中使用模拟的 API
|
|
|
|
|
+import Taro from '@tarojs/taro'
|
|
|
|
|
+
|
|
|
|
|
+describe('小程序功能', () => {
|
|
|
|
|
+ it('应该调用导航API', () => {
|
|
|
|
|
+ Taro.navigateTo({ url: '/pages/index' })
|
|
|
|
|
+ expect(Taro.navigateTo).toHaveBeenCalledWith({ url: '/pages/index' })
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
## 项目特定测试指导
|
|
## 项目特定测试指导
|
|
|
|
|
|