前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【干货分享】微信小程序单元测试攻略

【干货分享】微信小程序单元测试攻略

作者头像
WeTest质量开放平台团队
发布2021-12-17 10:40:11
2.6K0
发布2021-12-17 10:40:11
举报
文章被收录于专栏:WeTest质量开放平台团队的专栏

导语

本文作者是腾讯社交增值产品部高级前端工程师林毅雄,对前端开发领域颇有研究。接下来,本文将从测试框架、实战、覆盖率、踩坑等方面分享一下微信小程序的单元测试经验,希望能帮到大家。

01

写作初衷

大家先看看A公司与B公司的数据对比:

从上图可以看出,B公司的单元测试做的比较好,每百行error数也比A公司的项目低。

总体来说,单元测试有以下一些好处:

1,及早发现代码错误,提高代码质量和可维护性。

2,代码变更时可以快速进行检查。

然而要做好测试也有一定的困难:

1,花费时间长。

2,被测代码包含复杂的环境因素需要处理或模拟,例如使用了storage、调用了接口、使用了环境变量等。

(图片来源:码农翻身公众号)

但无论如何,有价值的东西就应该去做,不应该知难而退嘛。

接下来给大家介绍一下测试框架。

02.微信小程序测试框架

miniprogram-simulate

这是微信小程序自定义组件测试工具集。主要提供以下功能方便测试:

1.模拟 touch 事件、自定义事件触发。

2.选取子节点。

3.更新自定义组件数据。

4.触发生命周期。

2.1

架构

(图片来源:掘金技术社区)

2.2

接入

2.21 安装

代码语言:javascript
复制
// 小程序工具集$ npm i --save-dev miniprogram-simulate// Jest测试框架$ npm i --save-dev jest

2.2.2 在package.json中,添加测试相关命令

代码语言:javascript
复制
{sd    ...    script: {        "test": "jest --coverage"    }    ...}

2.2.3 添加jest.config.js:

注意testEnvironment设为 'jsdom',因为框架使用的是这个环境,如果配错会运行不起来。

代码语言:javascript
复制
// 更多配置查看:https://jestjs.io/docs/zh-Hans/configurationmodule.exports = {  verbose: true,  modulePathIgnorePatterns: [    '<rootDir>/dist-wx/',    '<,rootDir>/node_modules/',  ],  // 是否开启自动mock测试文件中导入的文件  automock: false,  testRunner: 'jasmine2',  // 测试文件执行前会先执行该文件,用来给Jest测试函数加代理从而收集测试用例  setupFilesAfterEnv: ['./node_modules/@tencent/dwt/dist/src/testbase/testbase.js'],  // 覆盖率报告依赖  reporters: [    'default',    '@tencent/dwt-reporter',  ],  // 测试文件匹配规则  testMatch: [    '**/__test__/**/*.test.ts?(x)',  ],  // 测试覆盖报告文件列表,下面是默认列表  coverageReporters: ['json', 'lcov', 'text', 'clover'],  // 全局变量配置  globals: {    NODE_ENV: 'test',    __wxConfig: {      global: {        window: {},      },    },  },  moduleNameMapper: {    '@/(.*)$': '<rootDir>/$1.ts',  },  setupFiles: ['<rootDir>/__test__/wx.ts'],  transform: {    '^.+\\.[jt]s?$': 'ts-jest',  },  preset: 'ts-jest',  testEnvironment: 'jsdom',  collectCoverage: true,  coverageDirectory: './__test__/coverage',  coverageReporters: ['json-summary', 'text', 'lcov'],  coveragePathIgnorePatterns: [    '/node_modules/',  ],  moduleNameMapper: {    '^@/(.*)$': '<rootDir>/$1',  },  coverageThreshold: {    global: {      branches: 50,      functions: 50,      lines: 50,      statements: 50,    },  },};

2.3

组件测试

示例:如何给一个提现弹窗写组件测试?

代码语言:javascript
复制
// 自定义组件 JS逻辑Component({    properties: {        showDialog: Boolean,  // 是否展示Dialog        title: String, // Dialog标题        okText: String, // 确认按钮文案  cancelText: String, // 取消按钮文案        showCancel: Boolean, // 是否展示取消按钮        confirmStyle: String, // 确认按钮Style  num: Number,    },    methods: {        ok() {      // 提现逻辑            this.triggerEvent('ok');        },  onCancel() {            this.triggerEvent('cancel');        }    }});

2.3.1 要考虑的事项

1. 根据组件传入的属性有相对应的DOM表现。

传入不同的属性值, 其组件产生的内容、结构、样式变化也是可预计的,例如:

• 根据showCancel属性值, 判断Cancel按钮是否展示。

• title, text, okText, cancelText文案是否一致。

• confirmStyle, titleStyle的值与实际样式是否一致。

2. 响应用户交互触发事件。

处理用户操作, 保证事件触发时, 响应函数如预期,例如:

• onOk 当用户点击确认按钮时触发。

• onCancel 当用户点击取消按钮时触发。

2.3.2 实践

2.3.2.1环境初始化

代码语言:javascript
复制
import path from 'path';import { load, render, extendRequest } from 'miniprogram-simulate';describe('[wx-component]dialog', () => {  // ...}

2.3.2.2 确认组件属性是否与DOM里的内容、结构、样式相同

代码语言:javascript
复制
it('[dialog] 属性文案渲染正常', () => {
    const id = load(path.join(__dirname, '../dist-wx/components/dialog/index'));
    // comp是渲染后的组件树    const comp = simulate.render(id, {        showDialog: true,        title: '请确认提现金额',    num: '0.03',        okText: '确认提现',    cancelText: '取消',        showCancel: false,    confirmStyle: 'background-color: blue;'    });
    // 组件树提供querySelector,支持class查询    const title = comp.querySelector('.main-title');    const okBtn = comp.querySelector('.ok-btn');    const cancelBtn = comp.querySelector('.cancel-btn');
    // 内容检测断言    // toContain, 检测内容中是否包含预期内容    expect(title.innerHTML).toContain('请确认提现金额');    expect(okBtn.innerHTML).toContain('确认提现');    expect(cancelBtn.innerHTML).toContain('取消');
    // 结构检测断言    // 判断取消按钮是否按预期不存在    expect(cancelBtn).toBeUndefined();
    // 样式检测断言    // 判断确认按钮样式是否按预期是蓝色    expect(window.getComputedStyle(okBtn.dom).backgroundColor).toBe('blue');});

querySelector支持以下查找节点的方式:

1. ID 选择器:#the-id

2. class 选择器(可以连续指定多个):.a-class.another-class

3. 子元素选择器:.the-parent > .the-child

4. 后代选择器:.the-ancestor .the-descendant

5. 跨自定义组件的后代选择器:.the-ancestor >>> .the-descendant

6. 多选择器的并集:#a-node, .some-other-nodes

2.3.2.3 确认用户操作是否正确响应:

要感知事件是否响应,我们需要使用spyOn方法。该方法和sinon.spy一样,生成函数的“间谍”,可以断言该函数的已调用次数、调用入参、调用返回等是否符合预期。

代码语言:javascript
复制
it('[dialog]btn event', async () => {    const comp = simulate.render(id, {        showDialog: true,        title: '请确认提现金额',        okText: '确认提现',    num: '0.03',        cancelText: 'Cancel',        showCancel: true    });
    // 分别监控 ok, onCancel, cancelDialog函数    const spyOk = jest.spyOn(comp.instance,"getData");;    const spyCancel = jest.spyOn(comp.instance, 'onCancel');    const spyHide = jest.spyOn(comp.instance, 'cancelDialog');
    const ok = comp.querySelector('.ok-btn');    const cancel = comp.querySelector('.cancel-btn');    const mask = comp.querySelector('.dialog-mask');
    // 触发确认按钮的tap事件    ok.dispatchEvent('tap');    // 触发取消按钮的tap事件    cancel.dispatchEvent('tap');    // 触发mask的tao事件    mask.dispatchEvent('tap');
    // 模拟异步回调    await simulate.sleep(200);
    // 断言监控到的结果    expect(spyOk).toHaveBeenCalled();    expect(spyCancel).toHaveBeenCalled();    expect(spyHide).toHaveBeenCalled();});

页面本质上是特殊的组件,因此组件测试的方法也适用于页面测试。只是在调用方法的时候需要改为页面的方法,例如对于加载完事件,组件调用ready,页面调用onload。

2.3.3 完整的断言方法

2.3.4 模拟数据mock

当被测方法包含环境因素不能直接测试时,例如使用了localStorage,又或者被测方法调用了接口,不希望测试时调用接口影响业务或降低测试速度,可以通过mock来模拟数据。

模拟接口调用示例:

代码语言:javascript
复制
// 被测代码A
import axios from 'axios'
export function getData() {  return axios.get('/api').then(res => res.data)}
// 测试代码B
import axios from 'axios';jest.mock('axios');
// 模拟一次接收到的数据axios.get.mockResolvedValueOnce({  data: '123'})
const data1 = await getData()expect(data1).toBe('123')

模拟storage调用示例1:

代码语言:javascript
复制
// 扩展 wx.getStorage 方法simulate.extendApi(  "getStorage", //API 名称  { key: `my_storage_key` }, //API 参数  { data: {} } //API 返回结果);

模拟storage调用示例2:

代码语言:javascript
复制
const mockStorage = {  get: jest.fn(),  set: jest.fn(),  remove: jest.fn(),};jest.mock('../src/storage.js', () => mockStorage);
mockStorage.get.mockImplementationOnce(() => JSON.stringify({    value,    expire: new Date('2030-1-1').getTime(),}));

它们是怎么mock方法的呢?其实是在mock的时候,就将这个方法放在cache中,当其他地方要import方法时,会先查看cache中有没有该方法,如果我们有mock了,他就使用mock的方法了。如果cache中没有该方法,再使用正常的方式import。

可以看看以下简化的原理:

代码语言:javascript
复制
const jest = {  mock(mockPath, mockExports = {}) {    const path = require.resolve(mockPath, { paths: ["."] });    require.cache[path] = {      id: path,      filename: path,      loaded: true,      exports: mockExports,    };  },};

const jest = {  fn(impl = () => {}) {    const mockFn = (...args) => {      mockFn.mock.calls.push(args);      return impl(...args);    };    mockFn.originImpl = impl;    mockFn.mock = { calls: [] };    return mockFn;  },};

2.3.5 更多组件测试方法

调用组件实例的 setData 方法:

代码语言:javascript
复制
comp.setData({ text: 'a' }, () => {})

触发组件实例的生命周期钩子:

代码语言:javascript
复制
comp.triggerLifeTime('ready')

扩展getApp()的返回结果,当组件中需要使用全局数据时,可通过该方式进行mock:

代码语言:javascript
复制
const extendAppData = require("../app.data.json");simulate.extendApp(extendAppData);

扩展getCurrentPages()的返回结果,当组件中需要使用页面栈数据时,可通过该方式进行mock:

代码语言:javascript
复制
simulate.extendCurrentPages(["pages/index/index.html"]);

模拟元素滚动:

代码语言:javascript
复制
simulate.scroll(comp, 100, 15) // 纵向滚动到 scrollTop 为 100 的位置,期间会触发 15 次 scroll 事件

获取符合给定匹配串的所有节点,返回Componment实例列表:

代码语言:javascript
复制
const childComps = comp.querySelectorAll('.a')

03

覆盖率

3.1 覆盖率包括

1. 行覆盖率(line coverage):是否每一行都执行了?

2. 函数覆盖率(function coverage):是否每个函数都调用了?

3. 分支覆盖率(branch coverage):是否每个if代码块都执行了?

4. 语句覆盖率(statement coverage):是否每个语句都执行了?

3.2 覆盖率监测原理

插桩代码进行采集。

3.3 覆盖率监测原理

使用“jest --coverage”进行覆盖率测试时,会在项目里生成覆盖率报告:

给人看的:

报告示例:

04

踩坑日志

4.1 load的id为null、render组件undefined

load的路径必须为dist后的文件。

4.2 s-jestjest-transfoemer Got a .js file to compile while allowJs option is not set tou true(file:/dist-wx/components/game-earnings/index/js).

tsconfig.ts添加:

代码语言:javascript
复制
"allowJs": true,

4.3 cannot find module 'path'/'__dirname' or its corresponding type declarations.

安装@types/node

tsconfig.ts添加:

代码语言:javascript
复制
"typeRoots": ["node_modules/@types/node",],

4.4 Module'"path"can only be default-imported using the 'esModuleInterop'flag

tsconfig.ts添加:

代码语言:javascript
复制
"esModuleInterop": true,

4.5 找不到名称"document"

tsconfig.ts添加:

代码语言:javascript
复制
"lib": ["dom"],

4.6 解决小程序编译与单测运行的类型定义重复问题:Cannot redeclare block-scoped variable 'require'

因为小程序编译时需要wx库,单测时需要node库,他们有一些相同的变量声明。解决办法:

tsconfig.ts添加:

代码语言:javascript
复制
"skipLibCheck": true,

05.实验性测试

小程序真机测试

5.1 使用框架

miniprogram-automator

5.2 框架功能

1. 操作 IDE(如打开开发者工具、打开小程序、关闭开发者工具、关闭小程序等)

2. 调用小程序 API (如 navigateTo、getSystemInfo 等)

3. mock 小程序 api 调用结果

4. evaluate(向逻辑层注入代码片段并返回执行结果)

5. 对页面元素进行操作(如 获取元素、获取属性、滑动 等)

5.3 简要流程

5.4 详细流程

关于腾讯WeTest

腾讯WeTest是由腾讯官方推出的一站式品质开放平台。十余年品质管理经验,致力于质量标准建设、产品质量提升。腾讯WeTest为移动开发者提供兼容性测试、云真机、性能测试、安全防护等优秀研发工具,为百余行业提供解决方案,覆盖产品在研发、运营各阶段的测试需求,历经千款产品磨砺。金牌专家团队,通过5大维度,41项指标,360度保障您的产品质量。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-12-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯WeTest 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云开发 CloudBase
云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档