最近接手了一个大坑项目——快应用。
开发体验十分痛苦,忍受着写完了一期,中间踩坑填坑不少。
当然,目前来看,其实最不能忍受的坑就是:coding 30s,编译3分钟。
有时为了验证一些效果,尤其是需要调出来的那类效果,花费的时间成本都在等待编译上。所以,快应用?真的快吗?呵呵😄。
牢骚发完,下面说说后面发现的一些等稍微缓解这类问题的方案。
虐我千遍,待如初恋。控制好自己能做的事情就行了。
抽离 & 复用
项目中一定存在一些较复杂的JS,比如处理各种字符串、各种转换、时间parse。
这部分代码是可以抽离到一个公共的地方,然后再其他需要进行相应的字符处理的地方,引入这些方法就行了。
对于快应用这类体量的项目,其实本身也不会太复杂庞大。做好抽离 & 复用,其实是有不少好处的。
app.ux
中有引入一个util.js
。
官方提供的这套例子中,把公共方法放入util中,再通过app .ux进行引入。
项目结构如下:
1
2
3
4
5
6
7
8
|
├── src
│ ├── Page 页面目录
│ | └── index.ux 页面文件,可自定义页面名称
│ ├── app.ux 项目入口
| |—— util.js 公共方法util
│ └── manifest.json 项目配置文件,配置应用图标、页面路由等
└── package.json 定义项目需要的各种模块及配置信息
|
单元测试
抽出来的方法,就很容易来做单元测试了。
但是在快应用的场景我们还需要进行一些额外处理。
快应用demo的util中引入的自身的模块,是无法mock的。
1
2
3
4
5
6
7
|
import prompt from '@system.prompt'
import router from '@system.router'
import app from '@system.app'
import shortcut from '@system.shortcut'
import fetch from '@system.fetch'
import network from '@system.network'
|
这部分东西利用到快应用自身的功能,由于这些module只在快应用的app里,即不存在node_module
中也不在其他模块中,所以是无法利用常规思路进行mock的,因为测试框架无法到达这些模块。
但是util中存在一些处理其他业务的代码,而且相对来说更复杂也容易出问题,所以此时需要修改结构,将没有引入快应用自身功能的代码再独立成其他的模块,这样就可以进行单元测试了。
我们修改了代码,把相应部分都移到helper.js
中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//helper.js
import {
toThousands,
trim,
validPhone,
validateEmail,
testEmoji,
truncate,
generateUUID,
phoneFormat,
toDecima12,
bankCardFormat
} from './helper'
|
这样的代码就是容易进行测试的代码。
配置JEST
我们选用jest进行测试。
首先需要安装相应j模块。
npm install jest@22.4.4
。
在package.json
的script部分加入如下配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
{
...
"scripts": {
....
"test":"jest"
},
"dependencies": {
...
},
...
}
|
在项目根目录下创建jest的配置文件 jest.config.js
。
加入以下内容:
1
2
3
4
5
6
|
module.exports = {
verbose: false, // 不启用verbose
collectCoverage: true, // 收集覆盖率
coverageDirectory: './coverage/', // 覆盖率报告输出路径
};
|
然后在src下创建 __test__
文件夹,jest会自动从这个文件夹内查找测试代码。
最终,项目结构会变成这样:
1
2
3
4
5
6
7
8
9
10
11
12
|
├── src
|—— |—— __test__ jest测试目录
│ ├── Page 页面目录
│ | └── index.ux 页面文件,可自定义页面名称
│ ├── app.ux 项目入口
| |—— util.js 公共方法util
| |—— helper.js 无依赖helper模块
│ └── manifest.json 项目配置文件,配置应用图标、页面路由等
|—— jest.config.js jest 配置文件
└── package.json 定义项目需要的各种模块及配置信息
|
编写测试代码
在__test__
文件夹创建helper.test.js
文件。
比如helper.js
中有一个方法用来缩略字符串的,在超过的长度替换显示成 ...
:
1
2
3
4
5
6
7
8
9
10
11
12
|
//helper.js
/**
* 将字符串省略并加入
* @param str 字符串
* @param n 长度
* @return {string}
*/
export function truncate(str, n) {
return (str.length > n) ? str.substr(0, n - 1) + '...' : str;
}
|
我们可以这样对其做测试。
1
2
3
4
5
6
7
8
9
10
11
|
import { truncate } from '../helper'
describe('helper', () => {
it('should truncate string with length', () => {
const truncateString = truncate(`Don't panic`,6)
expect(truncateString).toBe(`Don't...`)
const truncateString2 = truncate(`Don't panic`,22)
expect(truncateString2).toBe(`Don't panic`)
})
})
|
执行npm run test
。得到如下结果:
因为在jest的config中我们配置了collectCoverage: true
, 所以这里输出了覆盖率报告,jest内置了istanbul
来生成覆盖率数据。
由于我们只对truncate
这个方法进行了测试,所以其他部分都是没有测试到的,
在项目下同时也生成了一份html版本的覆盖率报告。
这份报告很直观输出了目前对helper.js
的各个语句、逻辑分支、函数的测试情况的汇总,以及目前的测试覆盖了源码的哪一行。
现在我们测试一下phoneFormat
这个方法,phoneFormat
方法实现了如下需求:
- 将手机号格式化成
xxx xxxx xxxx
这样的344格式
- 输入到第三位的时候,插入空格与第四位数字分隔开
- 输入到第七位的时候,插入空格与第八位数字分隔开
phoneFormat
的实现代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
//helper.js
/**
* 格式化手机
* @param value
* @return {string}
*/
export function phoneFormat(value) {
if (value) {
let phoneNumber = value.replace(/\D/g, '')
if (phoneNumber.length > 11) {
return phoneNumber
} else {
let number = phoneNumber.substring(0, 11);
let numberLen = number.length;
if (numberLen > 3 && numberLen < 8) {
number = `${number.substr(0, 3)} ${number.substr(3)}`;
} else if (numberLen >= 8) {
number = `${number.substr(0, 3)} ${number.substr(3, 4)} ${number.substr(7)}`;
}
return number;
}
} else {
return "";
}
}
|
在helper.test.js
中,接在之前truncate
方法后面加入 phoneFormat
的测试代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//helper.test.js
import { truncate,phoneFormat } from '../helper'
describe('helper', () => {
it('should truncate string with length', () => {
...
})
it('should return parse phone format like xxx xxxx xxxx', () => {
const phone = phoneFormat(`13000000000`)
expect(phone).toBe(`130 0000 0000`)
})
})
|
我们看到加入了这部分测试代码后,覆盖率一下子就提升了上来。
一般来说,测试覆盖率在75以上,就能让项目的质量有了不少保证,一些基础架构核心类型的项目,覆盖率有可能要求达到100%。
这里的测试率报告也同时给出了,这个方法内哪些分支覆盖了测试,哪些没有。
细心的朋友会发现,这里多出了两种黑底黄字的图标,分别写了E和I。
把鼠标hover到96行的E图标上,会出现一句title:else path not taken。
这就是表明这里的 if{…} else{…}
语句我们只测试了if
部分,113行的else
部分代码的背景是红色的,说明目前的测试代码没有覆盖到else部分。
同理,99行的图标 I 当然就是 if path not taken。表明 :
1
2
3
|
if (phoneNumber.length > 11) {
return phoneNumber
}
|
这部分if逻辑分支,用于处理长度超过11的部分的代码并没被覆盖到。
我们接着在后面添加测试代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
//helper.test.js
import { truncate,phoneFormat } from '../helper'
describe('helper', () => {
it('should truncate string with length', () => {
...
})
it('should return parse phone format like xxx xxxx xxxx', () => {
const phone = phoneFormat(`13000000000`)
expect(phone).toBe(`130 0000 0000`)
const inputEmpty = phoneFormat(``)
expect(inputEmpty).toBe(``)
})
})
|
由于测试了else部分,所以我们的覆盖率中,语句和分支这部分都比之前提升了。打开覆盖率报告,发现 黑底黄字的E图标也没了,113行else部分的代码也不是红色背景了。
现在我们继续完善测试代码,比如模拟用户输入手机号到第4位和第7位时,是否正确加入了空格。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
//helper.test.js
import { truncate,phoneFormat } from '../helper'
describe('helper', () => {
it('should truncate string with length', () => {
...
})
it('should return parse phone format like xxx xxxx xxxx', () => {
const phone = phoneFormat(`13000000000`)
expect(phone).toBe(`130 0000 0000`)
const longphone = phoneFormat(`131234567890`)
expect(longphone).toBe(`131234567890`)
const input4number = phoneFormat(`1300`)
expect(input4number).toBe(`130 0`)
const input7number = phoneFormat(`130 0000`)
expect(input7number).toBe(`130 0000`)
const input8number = phoneFormat(`130 0000 0`)
expect(input8number).toBe(`130 0000 0`)
const inputmorethan8number = phoneFormat(`130 0000 000`)
expect(inputmorethan8number).toBe(`130 0000 000`)
const inputEmpty = phoneFormat(``)
expect(inputEmpty).toBe(``)
})
})
|
至此,phoneFormat
方法里全部分支以及语句都测试覆盖了。
按照以上方式,持续补全测试代码,
目前就只剩2个分支没有覆盖到了,其余部分都挺不错。
如果代码不易测试的话,那么就可能需要考虑重构了,当然,如何重构就是另一篇文章讨论的了,这里就暂不展开说了。
-EOF-