ArkTS1.2 build system test suite
本测试套件基于 Jest 编写,用于测试 build_system 的功能。 自动化测试套件可通过命令行调用,展示测试结果(包括通过/失败的测试数量、编译时间、字节码大小、峰值内存,增量编译识别),并显示失败测试的详细信息。
Jest 引入
1. 修改 build_system/package.json
添加测试脚本
在 package.json 的 scripts 项内添加:
"scripts": {
"build_system_Utest": "jest", // 单元测试,默认运行所有 test/ut 下的测试,可通过命令行传递测试文件位置运行指定测试,可修改 jest 配置灵活调整测试文件集
"build_system_Etest": "TEST=test/e2e/demo_hap jest test/e2e/compile.test.ts", // 端到端测试,针对编译项目过程进行测试,修改 TEST 参数指定被编译项目路径
}
添加测试依赖库
在 package.json 的 devDependencies 项内添加:
"devDependencies": {
"@babel/core": "^7.27.1", // 用于翻译TS代码,UT测试时使用babel可以避免tsc编译全部代码
"@babel/preset-env": "^7.27.2",
"@babel/preset-typescript": "^7.27.1", // 二者一起使babel能够识别和转译TS代码
"@types/jest": "^29.5.14", // Jest的类型定义文件,使typescript能识别jest的测试接口
"babel-jest": "^29.7.0", // 用于转译ts代码,不需编译测试ts文件
"jest": "^29.7.0", // 测试框架
"ts-node": "^10.9.2" // 用于在Node.js中运行TypeScript代码
}
babel的配置参数
babel使得运行测试前不需要tsc编译测试文件。
在 package.json 中添加:
"babel": {
"presets": [
["@babel/preset-env", { "targets": { "node": "current" } }],
"@babel/preset-typescript"
]
}
2. jest 的配置参数
在 build_system/jest.config.js 中添加:
module.exports = {
testEnvironment: "node",
verbose: true,
collectCoverage: true,
coverageDirectory: "<rootDir>/dist/coverage",
setupFilesAfterEnv: [
"<rootDir>/testHook/jest.memory-usage.js",
"<rootDir>/testHook/jest.time-usage.js",
"<rootDir>/testHook/jest.abc-size.js"
],
testMatch: [
"<rootDir>/test/ut/**/*.test.[jt]s"
],
testPathIgnorePatterns: []
};
运行测试套
测试套件分为单元测试(UT)和端到端测试(E2E)。
UT 测试示例
在 build_system/test/ut 下添加 osType.test.ts 文件:
import { isWindows, isLinux, isMac } from '../../src/utils';
describe('osType', () => {
it('should detect OS type correctly', () => {
expect(isWindows()).toBe(false);
expect(isLinux()).toBe(true);
expect(isMac()).toBe(false);
});
});
**[可选]**修改 build_system_Utest的testMatch参数为测试文件:
"scripts": {
"build_system_Utest": "jest --testMatch='**/test/ut/**/osType.test.ts' --testPathIgnorePatterns='test/e2e/'",
}
命令行运行:
npm run build_system_Utest
推荐的写法
每一个测试文件在ut下新建一个同名文件夹,例如 osType.test.ts 在 ut/osType 下新建一个 mockConsoleLog.test.ts 文件,采用小驼峰方式命名。
目前存在的UT
针对src下的文件有对应的同名Test测试,放在与src同名的文件夹内。 mock夹是一部分测试需要用到的mock文件。 默认配置下会运行这里全部测试文件,可根据实际需要调整jest配置进行筛选。
test/ut
├── base_modeTest
│ └── base_mode.test.ts
├── build_framework_modeTest
│ └── build_framework_mode.test.ts
├── compile_WorkerTest
│ └── compile_worker.test.ts
├── compile_thread_workerTest
│ └── compile_thread_worker.test.ts
├── declgen_workerTest
│ └── declgen_worker.test.ts
├── entryTest
│ └── entry.test.ts
├── fileManagerTest
│ └── filemanager.test.ts
├── generate_arktsconfigTest
│ └── generate_arktsconfig.test.ts
├── loggerTest
│ └── logger.test.ts
├── mock
│ ├── a.ets
│ └── mockData.ts
├── plugins_driverTest
│ └── plugins_driver.test.ts
├── process_build_configTest
│ └── process_build_config.test.ts
├── safeRealpath.test.ts
└── utilsTest
└── utils.test.ts
E2E 测试前置步骤
E2E 测试前的配置步骤如下:
- 进入 build_system 目录
cd <path_to_build_system>
-
执行mock_sdk操作,保证本地可以编译 build_system 本身和利用 build_system 编译 hap 包:
- SDK可以从每日构建下载,或者自行编译。
- 下载
ohos-sdk-pulib_0328,解压获得ohos-sdk目录,进入ohos-sdk/Linux解压ets-linux-x64-6.0.0.36-Canary1.zip获得ets目录。 ets/static即为SDK目录,将其中的内容复制进build_system/test/mock_sdk即可。- 复制完成后mock_sdk目录下应存在
api,arkts,build-tools,kits四个文件夹。
-
为mock_sdk/build-tools/ets2panda/bin下的几个可执行文件提供执行权限。
chmod +x test/mock_sdk/build-tools/ets2panda/bin/*
- 将所有端到端测试代码中的
${absolute_path_to_build_system}替换为实际的目录。
find test -name 'build_config*.json' -exec sed -i 's|${absolute_path_to_build_system}|'"$(pwd)"'|g' {} +
find test -name 'decl-fileInfo.json' -exec sed -i 's|${absolute_path_to_build_system}|'"$(pwd)"'|g' {} +
- 导出LD_LIBRARY_PATH环境变量,如果不想每次都手动导出可以写进环境变量文件,注意替换成实际的目录。
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:<your_path_to_build_system>/test/mock_sdk/build-tools/ets2panda/lib
- 下载依赖和编译src下所有代码。
npm install
npm run build
-
**[可选]**通过修改 jest.config.js 中的 jest 参数,自定义调整 test suite 的运行方式,例如包含、排除测试文件:
module.exports = { testEnvironment: "node", verbose: true, collectCoverage: true, // 收集代码覆盖率,打开此选项会大幅影响测试速度 coverageDirectory: "<rootDir>/coverageReport", // 调整代码覆盖率报告产物的位置 setupFilesAfterEnv: [ "<rootDir>/testHook/jest.memory-usage.js" // "<rootDir>/testHook/jest.time-usage.js" ], // 只加载内存监控的hook文件,时间监控的hook文件注释掉 testMatch: [ "test/ut/sum.test.ts" ], // 只运行一个测试 testPathIgnorePatterns: [ "/test/ut/skip/", // 排除 skip 目录 "/test/ut/sometest.test.ts" // 排除指定文件 ] }
E2E 测试示例
端到端测试的目的通常是检查一个项目代码是否能通过编译,正常生成abc文件,并且检查编译过程中是否有错误或异常。端到端测试与单元测试的运行方式类似,只是提供了不同的接口文件。
在 build_system/test/e2e 下添加 compile.test.ts 文件,该代码已在项目文件中包含:
// 文件过大,仅展示部分
import { execFile } from 'child_process';
import { promisify } from 'util';
import fs from 'fs';
import path from 'path';
const execFileAsync = promisify(execFile);
function getAllFilesWithExt(dir: string, exts: string[]): string[] {
if (!fs.existsSync(dir)) return [];
let result: string[] = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
result = result.concat(getAllFilesWithExt(fullPath, exts));
} else if (exts.some(ext => entry.name.endsWith(ext))) {
result.push(fullPath);
}
}
return result;
}
// 下略
考虑到待测试项目可能非常复杂,可能存在多个build_config.json文件,且无法得知具体的编译先后顺序,于是强制用户编写运行脚本指定编译流程。 运行方式如下:
单个测试运行
修改 build_system_Etest 脚本的 TEST 参数为测试项目脚本:
"scripts": {
"entry1_2_external_har1_2:gen_abc": "npm run build && node ./dist/entry.js ${absolute_path_to_build_system}/test/e2e/entry1_2_external_har1_2/build_config.json",
"build_system_Etest": "TEST=entry1_2_external_har1_2:gen_abc jest --testMatch '${absolute_path_to_build_system}/test/e2e/*.test.ts'"
}
在TEST=后添加脚本的完整名字。
命令行运行:
npm run build_system_Etest
会执行测试。
添加测试的方式:
在 build_system/test/e2e 下新建一个测试文件,build_system_Etest会自动包含测试,或是通过调整jest配置筛选待执行测试文件,(实际上与端到端测试类似)。
在现有的测试中有一个较为独特,IncrementDemo:gen_abc这个测试与增量编译相关,于是为这个测试提供了单独的命令:
npm run IncrementCompileTest1
npm run IncrementCompileTest2
多个测试运行
在test/e2e下有run_all.sh脚本,该脚本会循环运行全部测试文件,通过增删scripts来控制运行的测试。
scripts1=(
"demo_entry1.1_har1.1_hsp1.2:gen_abc"
"demo_entry1.1_har1.2_hsp1.1:gen_abc"
"demo_entry1.1_har1.2_hsp1.2:gen_abc"
"demo_entry1.1_hsp1.2:gen_abc"
"demo_entry1.2_har1.1_hsp1.1:gen_abc"
"demo_entry1.2_har1.1_hsp1.2:gen_abc"
"demo_entry1.2_har1.2_hsp1.1:gen_abc"
"demo_entry1.2_har1.2_hsp1.2:gen_abc"
"demo_entry1.2_hsp1.1:gen_abc"
"demo_entry1.2_hsp1.2:gen_abc"
"demo_entry1_2:gen_abc"
"demo_har1_2:gen_decl"
"demo_hsp1_2:gen_decl"
"entry1_1_external_har1_2:gen_abc"
"entry1_1_external_har1_2:gen_decl"
"entry1_1_external_hsp1_2:gen_abc"
"entry1_1_external_hsp1_2:gen_decl"
"entry1_2_external_har1_1:gen_abc"
"entry1_2_external_har1_2:gen_abc"
"entry1_2_external_hsp1_1:gen_abc"
"entry1_2_external_hsp1_2:gen_abc"
"entry1_2_external_hsp1_2:gen_decl"
)
scripts2=(
"IncrementCompileTest1"
"IncrementCompileTest2"
)
passed=()
failed=()
for script in "${scripts1[@]}"; do
echo "Running E2E test: $script"
TEST=$script npx jest --testMatch='**/test/e2e/*.test.ts' --testPathIgnorePatterns='test/ut/'
#npm run "$script"
if [ $? -eq 0 ]; then
passed+=("$script")
else
failed+=("$script")
fi
done
for script in "${scripts2[@]}"; do
echo "Running IncrementalCompile test: $script"
npm run "$script"
if [ $? -eq 0 ]; then
passed+=("$script")
else
failed+=("$script")
fi
done
echo
echo "================== E2E Test Summary =================="
total=$(( ${#scripts1[@]} + ${#scripts2[@]} ))
echo "Total: $total"
echo "Passed: ${#passed[@]}"
echo "Failed: ${#failed[@]}"
if [ ${#passed[@]} -gt 0 ]; then
echo "Passed tests:"
for s in "${passed[@]}"; do
echo " $s"
done
fi
if [ ${#failed[@]} -gt 0 ]; then
echo "Failed tests:"
for s in "${failed[@]}"; do
echo " $s"
done
fi
echo "======================================================"
确保脚本有执行权限
chmod +x test/e2e/run_all.sh
运行脚本:
./test/e2e/run_all.sh
会运行scripts数组中的所有测试脚本,并输出测试结果。 考虑到输出会较为复杂,建议将输出重定向到文件中,或是对输出结果进行筛选。
Obfuscation E2E测试
package.json中已经定义的Obfuscation相关测试项
"scripts": {
"obfuscation_config_demo_hap:gen_abc": "node ./dist/entry.js test/e2e/obfuscation_config_demo/entry/build_config.json",
"obfuscation_config_demo_har:gen_abc": "node ./dist/entry.js test/e2e/obfuscation_config_demo/har/build_config.json",
"obfuscation_config_demo_har2:gen_abc": "node ./dist/entry.js test/e2e/obfuscation_config_demo/har2/build_config.json",
"obfuscation_config_demo_hsp:gen_abc": "node ./dist/entry.js test/e2e/obfuscation_config_demo/hsp/build_config.json",
"obfuscation_config_demo_hsp_har:gen_abc": "node ./dist/entry.js test/e2e/obfuscation_config_demo/hsp_har/build_config.json",
}
单个测试运行
修改 build_system_Etest 脚本的 TEST 参数为测试项目脚本:
"scripts": {
"build_system_Etest": "TEST=obfuscation_config_demo_har:gen_abc jest --testMatch='**/test/e2e/*.test.ts' --testPathIgnorePatterns='test/ut/'",
}
在TEST=后添加脚本的完整名字。
命令行运行:
npm run build_system_Etest
会执行测试。
多组Obfuscation测试执行
确保脚本有执行权限
chmod +x test/e2e/run_obfuscation.sh
运行脚本:
./test/e2e/run_obfuscation.sh
会运行scripts数组中的所有测试脚本,并输出测试结果。
E2E_Obfuscation测试说明
- 先进行环境配置
- 将所有端到端测试代码中的
${absolute_path_to_build_system}替换为实际的目录。
find test/e2e_obfuscation -name 'build_config*.json' -exec sed -i 's|${absolute_path_to_build_system}|'"$(pwd)"'|g' {} +
find test/e2e_obfuscation -name 'build_config.json' -exec sed -i 's|"buildSdkPath": "."|"buildSdkPath": "'"$(pwd)/test/mock_sdk/"'"|g' {} +
- 添加ark工具
- 在<path_to_build_system>/test/mock_sdk/build-tools/ets2panda/bin目录下添加ark
独立仓根路径下进行:
- 编译ark
./ark.py x64.release arkcompiler/runtime_core/static_core/panda:arkts_bin --gn-args="abckit_enable=true"
-
拷贝ark到指定路径 其中ark文件的编译输出路径如下: ark:out/x64.release/arkcompiler/runtime_core/ark 把生成的ark放到<path_to_build_system>/test/mock_sdk/build-tools/ets2panda/bin路径下。
-
确保有执行权限:
chmod +x arkcompiler/ets_frontend/ets2panda/driver/build_system/test/mock_sdk/build-tools/ets2panda/bin/ark
- 设置ark执行所需的库路径 下面的/path/code路径替换实际代码根目录:
export PROJECTROOT=/path/code
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PROJECTROOT/out/x64.release/arkcompiler/runtime_core/:$PROJECTROOT/out/x64.release/thirdparty/icu
- 在test/e2e_obfuscation目录下新增用例说明
- 首先建工程目录,例如:demo_entry1_2_obfuscationTest。
- 在工程目录下分别添加build_config.json用于编译构建和obfuscation-rules.txt用于混淆设置。
- 如果工程目录中的Index.ets需要main函数输出日志进行比较,那边在工程目录下需要添加logOut.expected.txt文件用于比较输出的日志。
- 在<path_to_build_system>/package.json文件的scripts内添加如下示例内容用于执行: "demo_entry1_2_obfuscationTest:gen_obf": "node ./dist/entry.js test/e2e_obfuscation/demo_entry1_2_obfuscationTest/build_config.json",
- 添加运行多个用例的脚本
-
在
<path_to_build_system>/test/e2e_obfuscation下添加run_all.sh脚本 该脚本可以通过增删scripts来控制运行指定的测试,也可以把is_run_all_test设为true,则运行全部混淆测试文件。 -
确保脚本有执行权限:
cd <path_to_build_system>
chmod +x test/e2e_obfuscation/run_all.sh
- 运行脚本:
./test/e2e_obfuscation/run_all.sh
- 运行下面脚本需安装jq:
sudo apt install jq
- 脚本内容如下:
# 用于设置执行全部用例还是指定用例:true时运行全部用例
is_run_all_test=false
scripts=(
"demo_entry1_2_obfuscationTest_1:gen_obf"
)
if [ $is_run_all_test = false ]; then
target_array=("${scripts[@]}")
else
# 读取JSON文件并匹配键
json_file="$(pwd)/package.json"
match_key="gen_obf"
declare -a matched_keys
# 使用jq提取所有键
keys=$(jq -r '.scripts | keys_unsorted[]' "$json_file")
# 遍历键并匹配
for key in $keys; do
if [[ $key == *"$match_key"* ]]; then
matched_keys+=("$key")
fi
done
# 输出匹配结果
echo "匹配到的键:"
for key in "${matched_keys[@]}"; do
echo "$key"
done
target_array=("${matched_keys[@]}")
fi
passed=()
failed=()
# 清理 dist/out 目录
if [ -d "dist/out" ]; then
rm -rf dist/out
echo "已清理 $(pwd)/dist/out 目录"
else
echo "$(pwd)/dist/out 目录不存在"
fi
for script in "${target_array[@]}"; do
echo "Running e2e_obfuscation test: $script"
TEST=$script npx jest --testMatch='**/test/e2e_obfuscation/obfuscation.test.ts' --testPathIgnorePatterns='test/ut/'
#npm run "$script"
if [ $? -eq 0 ]; then
passed+=("$script")
else
failed+=("$script")
fi
done
echo
echo "================== e2e_obfuscation Test Summary =================="
total=$(( ${#target_array[@]} ))
echo "Total: $total"
echo "Passed: ${#passed[@]}"
echo "Failed: ${#failed[@]}"
if [ ${#passed[@]} -gt 0 ]; then
echo "Passed tests:"
for s in "${passed[@]}"; do
echo " $s"
done
fi
if [ ${#failed[@]} -gt 0 ]; then
echo "Failed tests:"
for s in "${failed[@]}"; do
echo " $s"
done
fi
echo "======================================================"
如何编写测试
测试套基于Jest完成,利用Jest提供的断言和匹配器机制完成测试。
全局配置
- 按组组织测试:
describe(name, fn),多个测试包含在 fn 内,组成一个组。 - 单个测试:
it(name, fn),fn 内是测试的具体实现,所有 it 均可以替换为别名 test。 - 反向测试:
it.failing(name, fn, timeout),与 it 相反,fn 成功则失败,fn 失败则成功。 - todo:
it.todo(name),表示该测试未编写。 - 全部测试完成前/后执行操作:
afterAll(fn, timeout),beforeAll(fn, timeout),fn 放具体实现,可选 timeout 设置超时时间。 - 逐个测试进行前/后执行操作:
afterEach(fn, timeout),beforeEach(fn, timeout),fn 放具体实现,可选 timeout 设置超时时间。
断言
expect(expr)创建断言对象,例如expect(isLinux())
匹配器
匹配器是断言对象提供的方法,因此链式调用即可。Jest提供了大量匹配器,这里给出常用的几个:
- toBe(value): 检查值是否相等
- toHaveBeenCalled():检查是否被调用,常用于mock函数
- toHaveReturned():检查函数是否正常返回,常用于mock函数
- toHaveLength(number):检查数组长度
- toBeInstanceOf(Class):判断对象是否为类型示例,与instanceof类似
- toContain(item):检查包含性
- 等等,更多匹配器可以参考官方文档
mock 函数
在 build_system/test/ut/osType 下添加 osType.test.ts 文件:
import * as utils from '../../src/utils';
describe('osTypeCheck', () => {
it('should detect OS type correctly', () => {
expect(utils.isWindows()).toBe(false);
expect(utils.isLinux()).toBe(true);
expect(utils.isMac()).toBe(false);
});
it('mocked isWindows always return true', () => {
const spy = jest.spyOn(utils, 'isWindows').mockImplementation(() => true);
// mock整个模块,然后spyOn方法
expect(utils.isWindows()).toBe(true);
expect(utils.isLinux()).toBe(true);
expect(utils.isMac()).toBe(false);
spy.mockRestore();
});
});
考虑到ES6的导入是只读的,无法修改,因此只能用import * as utils 的方式引入模块。 需要mock模块还是直接mock函数需要具体情况具体分析。 mock 函数提供大量接口用于断言和检查,参考 官方文档
异步代码
返回 Promise 的函数,可以直接测试,若返回 Promise.resolve(),则测试通过,若返回 Promise.reject(),则测试失败。
测试结果
测试结果默认打印在命令行。 以上文的UT测试为例
> ArkTS2.0_build_system@1.0.0 build_system_Utest
> jest --testMatch='**/test/ut/**/osType.test.ts' --testPathIgnorePatterns='test/e2e/'
console.log
[Jest][osType should detect OS type correctly used 0.28 MB]
at Object.<anonymous> (test/testHook/jest.memory-usage.js:27:11)
console.log
[Jest][osType should detect OS type correctly spent 6 ms]
at Object.<anonymous> (test/testHook/jest.time-usage.js:26:11)
console.warn
[Jest][No .abc files found in ${absolute_path_to_build_system}/dist/cache]
42 | const abcFiles = getAllAbcFiles(cacheDir);
43 | if (abcFiles.length === 0) {
> 44 | console.warn(`[Jest][No .abc files found in ${cacheDir}]`);
| ^
45 | return;
46 | }
47 | abcFiles.forEach(file => {
at Object.<anonymous> (test/testHook/jest.abc-size.js:44:13)
PASS test/ut/osType.test.ts
osType
✓ should detect OS type correctly (8 ms)
---------------|---------|----------|---------|---------|---------------------------------------------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|---------------------------------------------------------
All files | 41.05 | 13.95 | 14.63 | 41.26 |
error.ts | 100 | 100 | 100 | 100 |
logger.ts | 6.75 | 0 | 0 | 6.84 | 25-103,125-126,147-179,189-218
pre_define.ts | 100 | 100 | 100 | 100 |
utils.ts | 36.76 | 0 | 23.07 | 36.76 | 51-53,57-60,64-71,77-78,82,87-95,99-109,114-120,128-143
---------------|---------|----------|---------|---------|---------------------------------------------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.903 s
Ran all test suites.
开头的三个console.log/warn是测试hook的输出,分别是内存使用情况、测试时间和abc文件大小。
格式为[Jest][${test_name} used ${memory} MB],
[Jest][${test_name} spent ${time} ms]和
[Jest][No .abc files found in ${cacheDir}]。
这里第三个输出是因为测试文件夹下没有abc文件,通常是因为测试没有进行编译工作。
表格为覆盖率的简略版本,分别显示每个文件的语句覆盖率、分支覆盖率、函数覆盖率和行覆盖率。
Test Suites的数量为执行的测试文件的数量。
Tests的数量为测试文件中test/it块的数量。
考虑到编译过程中也会有命令行输出,建议调整输出流的位置,例如将编译信息输出到临时文件中。
新增文件夹树
build_system
├── dist # 编译产物目录
│ └── coverage # 覆盖率产物目录
├── src # 源代码目录
├── test # 测试目录
│ ├── ut # 单元测试
│ │ ├── mockOsType # mock 函数测试
│ │ │ └── mockOsType.test.ts # mock 操作系统类型测试
│ │ ├── mockConsoleLog.test.ts # mock console.log 测试
│ │ └── ... # 更多单元测试
│ └── e2e # 端到端测试
│ ├── compile.test.ts # 编译测试
│ ├── checkHash.test.ts # 哈希检查测试
│ ├── abcGenerationTest # 示例 hap 项目
│ ├── ... # 更多端到端测试
│ └── testHook # 测试 hook 文件
│ ├── jest.memory-usage.js # 内存使用监控
│ ├── jest.time-usage.js # 测试时间监控
│ └── jest.abc-size.js # abc 文件大小监控
├── package.json # 项目配置文件
└── jest.config.js # Jest 配置文件
覆盖率报告
简略的覆盖率信息会打印到命令行。
详细的覆盖率报告默认输出到 dist/coverage 目录。可以通过修改 jest 配置中的 coverageDirectory 字段来调整输出位置。
Jest使用istanbul生成覆盖率报告。
覆盖率报告包括以下内容:
dist/coverage
├── clover.xml # Clover格式的覆盖率报告
├── coverage-final.json # Json格式的覆盖率报告
├── lcov-report # 详细的 HTML 格式覆盖率报告目录
│ ├── base.css
│ ├── block-navigation.js
│ ├── favicon.png
│ ├── index.html # 覆盖率报告,浏览器打开可查看详细覆盖率
│ ├── prettify.css
│ ├── prettify.js
│ ├── sort-arrow-sprite.png
│ └── sorter.js
└── lcov.info # 标准 LCOV 格式的覆盖率数据文件