0%

CocosCreator 中单元测试入门

CocosCreator 中单元测试入门

一、是什么

  单元测试是针对一个小模块(函数、类)做的检查。检查模块在执行后,它的输出是否符合期望。简单来说,就是测试咱们写的小模块正确。

二、为什么

  那为什么需要单元测试呢?因为它会带来很多好处。

  • 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、小结

  经过上面的总结,可以发现,本文讨论的单元测试,由于各类限制,仅适用于业务逻辑代码部分。这会强迫你将业务代码和界面交互代码分离,这对于清洁的代码而言,也是一个好消息。
  
  接下来我们开始动手准备单元测试了。

四、测试

1、环境准备

  在单元测试前,我们需要搭建后测试的环境。 主要是安装两个软件。   

4.1.1、NodeJs

  NodeJs 是我们测试代码运行起来的环境。下载地址是:https://nodejs.org/en/download/。建议安装 LTS 版本。   

4.1.2、npm

  npm 是 js 的包管理器。上面有很多第三方的 js 库。一般而言在安装 NodeJs 时,会附带安装好 npm。
  
安装验证:

1
2
node -v  # 会输出如: v12.16.1
npm -v # 会输出如:6.14.5

2、开始

4.2.1、CocosCreator 工程结构

  一个典型工程结构如下(去掉无关的文件及目录):

1
2
3
4
5
6
7
8
9
.
├── assets
│   ├── Scene
│   ├── Script
│   └── Texture
├── creator.d.ts
├── jsconfig.json
├── project.json
└── tsconfig.json

  我们写的代码一般都放到 ./assets/Script 目录下。咱们的工程里,测试的环境有一个要求:不能对 CocosCreator 工程本身造成影响。这意味着测试代码、配置不能让 CocosCreator 感知到。

4.2.2、初始化为 nodejs 工程

  咱们的单元测试是建立在 nodejs 工程上的,在工程的根目录下运行初始化命令:

1
npm init # 初始化 nodejs 工程

  经过交互,最终会生成一个 package.json 文件,其内容如下:

1
2
3
4
5
6
7
8
9
10
11
{
"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 下):

1
2
3
4
5
6
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"ls": "ls -al"
},
}

  现在,你可以在工程根目录下运行命令:npm run ls,这会在控制台打印出当前目录。扩展一下:咱们就可以方便地定义很多命令了,比如构造工程、图片压缩等。
  
  结合上面的内容,我们可以轻易地看到,运行测试的命令是:npm run test。其实 test 是一个特殊的命令,你还可以这么运行:npm test,而且它还有两个别名:npm t,这很方便。试一下:   

1
npm t # 输出:Error: no test specified

  由于测试环境没有搭建完成,所以给了上面的错误提示。

4.2.3、开始单元测试(jest)

  Nodejs 下的单元测试库很多。比如:mochajsjest 等,都是很优秀的库。本文采用 jest。其官网是:https://jestjs.io/docs/en/getting-started,上面的文档很详细,建议阅读。
  
  在项目根目录下运行命令安装 jest (ts 工程):

1
2
3
4
5
# 如果是ts工程:
npm install --save-dev ts-jest

# 如果是js工程:
npm install --save-dev jest

  这会在咱们工程里安装 jest 包,除此之外,还需要安装包:

1
2
3
4
npm i --save-dev @types/jest

# 如果是 ts 工程:
npm install --save-dev typescript

  @types/jest 用于在写单元测试时,在 vscode 中给出代码提示。
  
  安装好包后,修改测试命令(package.json文件):

1
2
3
4
5
{
"scripts": {
"test": "jest"
},
}

  运行测试命令试一下:

1
npm t #输出:No tests found

  运行成功,但 jest 没有找到需要进行的单元测试。
  
  我们先准备被测试的素材 ./assets/Script/util/readableNum.ts 文件:

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
33
34
35
36
37
38
39
/**
* 分割字符串
* @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 目录,建完后,如下:

1
2
3
4
5
6
7
8
9
10
11
12
.
├── 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
├── assets
│ ├── Scene
│ ├── Script
│ │ └── util
│ │ └── readableNum.ts
│ └── Texture
├── test
│ └── util
│ │ └── readableNum.test.ts
├── creator.d.ts
├── jsconfig.json
├── project.json
└── tsconfig.json

  编辑 readableNum.test.ts 内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 文件:

1
2
3
4
5
6
7
8
9
10
module.exports = {
preset: "ts-jest", // 如果是 js 工程,则是 "jest"
testEnvironment: 'node', // 测试代码所运行的环境
// verbose: true, // 是否需要在测试时输出详细的测试情况
rootDir: "./test", // 测试文件所在的目录
globals: { // 全局属性。如果你的被测试的代码中有使用、定义全局变量,那你应该在这里定义全局属性
window: {},
cc: {}
}
};

  
  好了,我们可以使用 npm t 测试了。如果你看到的是绿色的 PASS ,则表示测试通过啦。
  
  下图是一个测试通过的示例:

此处输入图片的描述