- 小程序单元测试
小程序单元测试小程序的测试和web应用测试区别不大,可以利用jest进行测试,但是由于jest只提供了nodejs和浏览器执行环境,因此小程序的api我们需要mock,下面讲解小程序测试的一些mock技巧。 mock小程序API我们测试小程序时,经常会调用微信api,例如wx.showLoading方法,但是因为我们的执行环境未定义该方法,会出现调用错误。 我们可以通过jest提供的global设置全局变量,可以在测试文件中单独编写,或者在package.json的jest块设置setupFiles属性,让jest自动加载。 [代码] "jest": { "setupFiles": ["./__tests__/wx.js"] },复制代码[代码]./tests/wx.js文件内容如下,表示将小程序的api方法定义为mock方法。 [代码]global.wx = { showLoading: jest.fn(), hideLoading: jest.fn(), showModal: jest.fn(), request: jest.fn(), getStorageSync: jest.fn(), showShareMenu: jest.fn(), };复制代码[代码]测试小程序页面[代码]// 空白的小程序页面代码 Page({ onLoad () { // your code } })复制代码[代码]一个空白的小程序页面,代码会被Page方法包裹,同时Page初始化后,会执行onLoad、onReady等生命周期方法,而且当前对象还能调用setData方法对页面data数据进行修改。 我们需要mock Page方法的实现,代码如下。 [代码]export const noop = () => {};export const isFn = fn => typeof fn === 'function';let wId = 0; global.Page = ({ data, ...rest }) => { const page = { data, setData: jest.fn(function (newData, cb) { this.data = { ...this.data, ...newData, }; cb && cb(); }), onLoad: noop, onReady: noop, onUnLoad: noop, __wxWebviewId__: wId++, ...rest, }; global.wxPageInstance = page; return page; };复制代码[代码]举个例子假设我们的小程序页面是一个电影列表展示,业务代码如下。 [代码]const filmServer = require('../../server/film.js'); Page({ data: { comingFilms: [], }, onLoad() { this.getComingFilm(); }, // 获取即将上映电影列表 getComingFilm() { return filmServer.getComingSoon(1, 5).then((data) => { data.films.forEach((film) => { const displayDate = `${new Date(film.premiereAt).getMonth() + 1}月${new Date(film.premiereAt).getDate()}日`; film.displayDate = displayDate; }); this.setData({ comingFilms: data.films }); }); }, });复制代码[代码]我们的编写两个测试用例保证代码的正确运行。1、保证onLoad时执行getComingFilm方法。2、保证getComingFilm后日期数据进行格式化。[代码]import '../../pages/film'; // 加载需要测试的页面 // 获取当前初始化的page对象,后续可用来调用setData等方法,类似小程序页面里的this。 const page = global.wxPageInstance; // mock网络请求 jest.mock('../../server/film.js'); describe('电影首页', () => { describe('onLoad', () => { beforeAll(() => { // spyOn后可使方法具有mock属性,同时不影响方法调用。 jest.spyOn(page, 'getComingFilm'); // 执行页面onLoad生命周期。 page.onLoad(); }); it('should getComingFilm', () => { // 断言onLoad后,是否执行了getComingFilm方法。因为我们前面已经将getComingFilm进行spyOn了,所以可以执行toBeCalled判断,否则会出错。 expect(page.getComingFilm).toBeCalled(); }); }); describe('getComingFilm', () => { it('should format premiereAt as MM月DD日 ', () => page.getComingFilm().then(() => { // 断言获取数据后,原始数据增加displayDate属性,格式化为MM月DD日 expect(page.data.comingFilms[0].displayDate).toEqual('9月12日'); })); }); });复制代码[代码]🌟🌟由于测试代码比较长,上面只截取了部分,完整代码可以访问github获取
2018-10-08 - setData 学问多
为什么不能频繁 setData 先科普下 setData 做的事情: 在数据传输时,逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将 setData 所设置的数据字段与 data 合并,使开发者可以用 this.data 读取到变更后的数据。 因此频繁调用,视图会一直更新,阻塞用户交互,引发性能问题。 但频繁调用是常见开发场景,能不能频繁调用的同时,视图延迟更新呢? 参考 Vue,我们能知道,Vue 每次赋值操作并不会直接更新视图,而是缓存到一个数据更新队列中,异步更新,再触发渲染,此时多次赋值,也只会渲染一次。 [代码]let newState = null; let timeout = null; const asyncSetData = ({ vm, newData, }) => { newState = { ...newState, ...newData, }; clearTimeout(timeout); timeout = setTimeout(() => { vm.setData({ ...newState, }); newState = null }, 0); }; [代码] 由于异步代码会在同步代码之后执行,因此,当你多次使用 asyncSetData 设置 newState 时,newState 都会被缓存起来,并异步 setData 一次 但同时,这个方案也会带来一个新的问题,同步代码会阻塞页面的渲染。 同步代码会阻塞页面的渲染的问题其实在浏览器中也存在,但在小程序中,由于是逻辑、视图双线程架构,因此逻辑并不会阻塞视图渲染,这是小程序的优点,但在这套方案将会丢失这个优点。 鱼与熊掌不可兼得也! 对于信息流页面,数据过多怎么办 单次设置的数据不能超过 1024kB,请尽量避免一次设置过多的数据 通常,我们拉取到分页的数据 newList,添加到数组里,一般是这么写: [代码]this.setData({ list: this.data.list.concat(newList) }) [代码] 随着分页次数的增加,list 会逐渐增大,当超过 1024 kb 时,程序会报 [代码]exceed max data size[代码] 错误。 为了避免这个问题,我们可以直接修改 list 的某项数据,而不是对整个 list 重新赋值: [代码]let length = this.data.list.length; let newData = newList.reduce((acc, v, i)=>{ acc[`list[${length+i}]`] = v; return acc; }, {}); this.setData(newData); [代码] 这看着似乎还有点繁琐,为了简化操作,我们可以把 list 的数据结构从一维数组改为二维数组:[代码]list = [newList, newList][代码], 每次分页,可以直接将整个 newList 赋值到 list 作为一个子数组,此时赋值方式为: [代码]let length = this.data.list.length; this.setData({ [`list[${length}]`]: newList }); [代码] 同时,模板也需要相应改成二重循环: [代码]<block wx:for="{{list}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 下拉加载,让我们一夜回到解放前 信息流产品,总避免不了要做下拉加载。 下拉加载的数据,需要插到 list 的最前面,所以我们应该这样做: [代码]this.setData({ `list[-1]`: newList }) [代码] 哦不,对不起,上面是错的,应该是下面这样: [代码]this.setData({ list: this.data.list.unshift(newList) }); [代码] 这下好,又是一次性修改整个数组,一夜回到解放前… 为了解决这个问题,这里需要一点奇淫巧技: 为下拉加载维护一个单独的二维数组 pullDownList 在渲染时,用 wxs 将 pullDownList reverse 一下 此时,当下拉加载时,便可以只修改数组的某个子项: [代码]let length = this.data.pullDownList.length; this.setData({ [`pullDownList[${length}]`]: newList }); [代码] 关键在于渲染时候的反向渲染: [代码]<wxs module="utils"> function reverseArr(arr) { return arr.reverse() } module.exports = { reverseArr: reverseArr } </wxs> <block wx:for="{{utils.reverseArr(pullDownList)}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 问题解决! 参考资料 终极蛇皮上帝视角之微信小程序之告别 setData, 佯真愚, 2018年08月12日
2019-04-11