【本文参与征文活动】
一、是什么
单元测试是针对一个小模块(函数、类)做的检查。检查模块在执行后,它的输出是否符合期望。简单来说,就是测试咱们写的小模块正确。
二、为什么
那为什么需要单元测试呢?因为它会带来很多好处。
- 1、保证小模块的编写是正确的;
- 2、大模块、程序是由小模块组成的,这也保证了程序的质量;
- 3、易于重构。有了单元测试,我们可以大胆的重构代码;
- 4、使代码更加清晰、简洁。
很多大神已经总结过单元测试的好处了。其本质是能减轻咱们开发的工作量,使工作变得轻松。
三、代码分类
我们需要在具体进行操作之前,先定义两个概念:
- 1、业务逻辑代码;
- 2、界面、用户交互代码;
- 3、原生代码。
在 CocosCreator 工程中,我们写的代码大致可以分为以上这几种。
3.1、业务逻辑代码
业务逻辑代码主要是一些业务计算逻辑。比如:数据建模、算法、字符串或数组等API的扩展。这些代码不会涉及到任何 CocosCreator 中的 API。我们的测试重点也在于这些代码。
3.2、界面、用户交互代码
凡是使用了 CocosCreator 中的 API,都可以归为此类代码。比如自定义的组件、cc.Label、cc.Sprite 等。
这些代码用户展示界面、处理用户的交互行为。一般依赖于开发、专业的测试人员进行白盒测试;另外,这些代码依赖了引擎 API,所以运行这些代码时,需要引擎代码参与,这对于单元测试是一个比较大的限制。所以此类代码不在本文的讨论范围。
3.3、原生代码
iOS、android 等原生代码。这些原生已经有成熟的测试工具,不在本文讨论范围。
3.4、小结
经过上面的总结,可以发现,本文讨论的单元测试,由于各类限制,仅适用于业务逻辑代码部分。这会强迫你将业务代码和界面交互代码分离,这对于清洁的代码而言,也是一个好消息。
接下来我们开始动手准备单元测试了。
四、测试
4.1、环境准备
在单元测试前,我们需要搭建后测试的环境。 主要是安装两个软件。
4.1.1、NodeJs
NodeJs 是我们测试代码运行起来的环境。下载地址是:https://nodejs.org/en/download/。建议安装 LTS 版本。
4.1.2、npm
npm 是 js 的包管理器。上面有很多第三方的 js 库。一般而言在安装 NodeJs 时,会附带安装好 npm。
安装验证:
node -v # 会输出如: v12.16.1
npm -v # 会输出如:6.14.5
4.2、开始
4.2.1、CocosCreator 工程结构
一个典型工程结构如下(去掉无关的文件及目录):
.
├── assets
│ ├── Scene
│ ├── Script
│ └── Texture
├── creator.d.ts
├── jsconfig.json
├── project.json
└── tsconfig.json
我们写的代码一般都放到 ./assets/Script
目录下。咱们的工程里,测试的环境有一个要求:不能对 CocosCreator 工程本身造成影响。这意味着测试代码、配置不能让 CocosCreator 感知到。
4.2.2、初始化为 nodejs 工程
咱们的单元测试是建立在 nodejs 工程上的,在工程的根目录下运行初始化命令:
npm init # 初始化 nodejs 工程
经过交互,最终会生成一个 package.json
文件,其内容如下:
{
"name": "testdemo",
"version": "1.0.0",
"description": "Hello world new project template.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
这里可以先关注上面的 scripts.test
属性。
scripts
下面的属性,都可以当作命令运行: npm run commond
。比如你可以添加一个显示目录的命令 (类 unix 下):
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"ls": "ls -al"
},
}
现在,你可以在工程根目录下运行命令:npm run ls
,这会在控制台打印出当前目录。扩展一下:咱们就可以方便地定义很多命令了,比如构造工程、图片压缩等。
结合上面的内容,我们可以轻易地看到,运行测试的命令是:npm run test
。其实 test
是一个特殊的命令,你还可以这么运行:npm test
,而且它还有两个别名:npm t
,这很方便。试一下:
npm t # 输出:Error: no test specified
由于测试环境没有搭建完成,所以给了上面的错误提示。
4.2.3、开始单元测试(jest)
Nodejs 下的单元测试库很多。比如:mochajs
、jest
等,都是很优秀的库。本文采用 jest
。其官网是:https://jestjs.io/docs/en/getting-started,上面的文档很详细,建议阅读。
在项目根目录下运行命令安装 jest (ts 工程):
# 如果是ts工程:
npm install --save-dev ts-jest
# 如果是js工程:
npm install --save-dev jest
这会在咱们工程里安装 jest
包,除此之外,还需要安装包:
npm i --save-dev @types/jest
# 如果是 ts 工程:
npm install --save-dev typescript
@types/jest
用于在写单元测试时,在 vscode 中给出代码提示。
安装好包后,修改测试命令(package.json
文件):
{
"scripts": {
"test": "jest"
},
}
运行测试命令试一下:
npm t #输出:No tests found
运行成功,但 jest
没有找到需要进行的单元测试。
我们先准备被测试的素材 ./assets/Script/util/readableNum.ts
文件:
/**
* 分割字符串
* @param str 将会被分割的字符串
* @param charCount 每 `charCount` 个会被分割
* @param divChar 分割字符。
*/
function division(str: string, charCount: number, divChar: string = ',') {
const chars: string[] = [];
let count = 0;
for (let i = str.length - 1; i >= 0; --i) {
chars.push(str[i]);
count++;
if (count % charCount == 0 && i !== 0) {
chars.push(divChar);
}
}
const result = chars.reverse().join('');
return result;
}
/**
* 使数字可读。如:1004213 -> '1,004,213'
* @param number
* @param divChar 分割字条符
*/
export function readableNum(number: number, divChar = ',') {
if (String(number).length <= 3) {
return String(number);
}
if (number < 0) {
return '-' + division(String(-1 * number), 3, divChar);
}
return division(String(number), 3, divChar);
}
现在来写针对该文件里的 readableNum
函数的单元测试。首页考虑的事:测试文件放到哪里呢?为了不对工程本身造成影响,不能放到 ./assets
目录下。可以在工程根目录下新建一个 test
目录,建完后,如下:
.
├── assets
│ ├── Scene
│ ├── Script
│ │ └── util
│ │ └── readableNum.ts
│ └── Texture
├── test
├── creator.d.ts
├── jsconfig.json
├── project.json
└── tsconfig.json
咱们写的单元测试文件都会放到该目录下。
在 jest
中,测试文件名与被测试文件名相同,文件名中间加上 test
,比如 readableNum.ts
的测试文件名应该是:readableNum.test.ts
;然后保持相同的目录结构。基于这样的规则,我们新建文件:./test/util/readableNum.test.ts
:
.
├── assets
│ ├── Scene
│ ├── Script
│ │ └── util
│ │ └── readableNum.ts
│ └── Texture
├── test
│ └── util
│ │ └── readableNum.test.ts
├── creator.d.ts
├── jsconfig.json
├── project.json
└── tsconfig.json
编辑 readableNum.test.ts
内容如下:
import { readableNum } from "../../assets/Script/util/readableNum";
test('readableNum', () => {
expect(readableNum(1000)).toBe('1,000');
expect(readableNum(10000)).toBe('10,000');
expect(readableNum(416506250)).toBe('416,506,250');
expect(readableNum(416506250, '.')).toBe('416.506.250');
expect(readableNum(416506250, '')).toBe('416506250');
expect(readableNum(-600 * 1000, '.')).toBe('-600.000');
expect(readableNum(-600 * 1000 * 1000, '.')).toBe('-600.000.000');
expect(readableNum(-121892262728, '.')).toBe('-121.892.262.728');
expect(readableNum(0)).toBe('0');
});
基于 jest
文档说明,上面测试简单说明一下:test
方法开启一个单元测试,第 1 个参数是名字,第 2 个参数是测试的内容。expect
方法参数中传入被测试的内容,toBe
是期望的结果。这里只是简单做的示范,jest
远比这里展示的丰富。
接下来,我们还得告诉 jest
一些我们的配置,让它去哪个目录找测试文件。在根目录下新建:./jest.config.js
文件:
module.exports = {
preset: "ts-jest", // 如果是 js 工程,则是 "jest"
testEnvironment: 'node', // 测试代码所运行的环境
// verbose: true, // 是否需要在测试时输出详细的测试情况
rootDir: "./test", // 测试文件所在的目录
globals: { // 全局属性。如果你的被测试的代码中有使用、定义全局变量,那你应该在这里定义全局属性
window: {},
cc: {}
}
};
好了,我们可以使用 npm t
测试了。如果你看到的是绿色的 PASS
,则表示测试通过啦。
下图是一个测试通过的示例: