最近接手了一个大坑项目——快应用。 开发体验十分痛苦,忍受着写完了一期,中间踩坑填坑不少。 当然,目前来看,其实最不能忍受的坑就是: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-