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

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

接下来我们开始动手准备单元测试了。

四、测试

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 下的单元测试库很多。比如:mochajsjest 等,都是很优秀的库。本文采用 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 ,则表示测试通过啦。

下图是一个测试通过的示例:

27赞

之前看过一篇 slides,可以把 cc 的部分处理掉,这样单测写起来尺度应该可以更大一些。
http://slides.com/mangogan/jestxccc/fullscreen

6赞

是的是的 可以想办法把引擎的代码集成进行的。

这样就可以测试没有界面依赖的 API 扩展了。

3赞

楼主还可以再试一下,在VSCode里面,集成可视化的Test Runner,比如下面这个
https://marketplace.visualstudio.com/items?itemName=kavod-io.vscode-jest-test-adapter

这样就可以直接在VSCode的UI里面,直接run这些Test,或者单独run某一个Test了。

3赞

前排学习下

前排战略mark。

即便是ts项目,不安装jest,运行npm t会报错

我的也报错:


配置文件也缺,我是直接百度搜的教程弄的

感谢反馈,你是对的

你这么安装再试一下:

npm i -D jest typescript
npm i -D ts-jest @types/jest

微信图片_20210120160237
遇到这个问题,各位有碰到过嘛。尝试修改了typescript的版本也无效

你可以试试用不同的 ts 版本,在 package.json 的文件中:

"devDependencies": {
    // ...
    "typescript": " 3.9.3"
  }

谢谢,是个人模块使用问题

cocos的require(‘xxx’)是全局的,这个在跑测试代码时找不到module怎么处理?

要通过相对路径引用

嗯,我知道了。先__mocks__里面加入同名文件,在这个文件里面直接
module.exports = require(’…/…/…/…/…/assets/ScriptFirst/MyLC’);
找这个位置的js

不过cc空间里的应该怎么办呢?

你说的『引擎的代码集成进行的』是什么意思?