开发者在进行业务代码开发时,需要关注自己开发的功能是否正常运作。尽管这些功能可能只是整个系统中的一小部分,但它们仍然需要经过单元测试(UT)、集成测试(IT)以及一定程度的真实环境测试。这些测试的目的不仅是验证新开发的单元功能是否正确,还要确保这些新功能在集成过程中与周围接口的正确性,并且没有破坏其他功能。
开发者在进行开发者测试代码编写时,要以提高产品业务代码的可测试性,提升DT用例设计、DT用例实现与执行质量;优化DT工程编译、运行、调试加载时间和降低DT工程维护成本,从而提升DT效率,降低DT成本,提升开发生产率为目标。
对于openUBMC的各个组件来说,它们在开发阶段就具备了独立测试的能力。开发者进行的测试可以显著减少版本集成后的缺陷,并减少后期定位缺陷的成本。开发者编写的测试代码与业务代码具有同等的重要性。
当前openUBMC DT测试框架主要涵盖的场景包括单元测试和集成测试:
- 单元测试主要关注组件内函数可靠性,主要测试对象为组件内部各个子模块之间互相调用的函数。
- 集成测试主要关注本组件与其他组件的接口可靠性,不关注内部函数。
openUBMC的开发工具BMC Studio
提供了开发者测试的CLI命令bingo test
此命令的相关参数如下:
Test component
optional arguments:
-h, --help show this help message and exit
-bt BUILD_TYPE, --build_type BUILD_TYPE
构建类型,可选:dt,debug,release 默认:debug
--stage STAGE 组件包发布阶段, 可选: dev(developer), pre, rc, stable 默认:dev
-r REMOTE, --remote REMOTE
conan仓别名,请检查conan remote list查看已配置的conan仓 默认: openUBMC_dev
-s, --from_source Build from source, include dependency component
-nc, --no_cache 不使用~/.conan/data目录下的缓存包(构建前删除缓存)
--user USER 指定conan包的user字段,尝试从/etc/bingo.conf文件读取conan.user字段,否则由
程序管理
-as, --asan Enable address sanitizer
-ut, --unit_test Enable unit test
-it, --integration_test
Enable integration test
-f TEST_FILTER, --test_filter TEST_FILTER
Run unit test with a filter
-a APP, --app APP App in hica
-wb, --without_build 测试时不重新构建
其中,bingo test -ut
和bingo test -it
分别单元测试和集成测试的运行命令,也是开发人员最常用的测试命令。
单元测试
单元测试(UT)又称为模块测试,是针对程序模块来进行正确性检验的测试工作。开发者需要为自己所要编写的代码和自动生成的代码编写测试用例,确保代码逻辑的正确性。
测试框架
openUBMC采用LuaUnit
开源软件作为单元测试的基础测试框架。它提供了用例管理、调度和断言等功能,是一个功能全面、易于使用的单元测试框架。该框架会分析引用的文件中,所有以Test
为开头的类,并依次调用以test_*
命名的测试函数。目前,openUBMC已将luaunit
转移至开发工具,在构建过程中会自动写入开发者测试依赖中,不需要开发人员自行配置。开发人员只需要在测试代码中导入LuaUnit
,编写相关测试代码即可:
luaunit = require('luaunit')
LuaUnit
提供了assertXX()
系列断言函数,比如assertNotNil()
, assertEquals()
等,可以提供更丰富的提示信息等。因此,在openUBM中,不直接使用assert()
函数。下面简要介绍常用的几种断言函数:
相等性断言:
-- 相等性断言。如果断言失败,将会打印extra_msg。对于table类型参数会进行更深层次的比较(元素个数、键、值比较)
assertEquals(actual, expected[, extra_msg])
-- 不等性断言。
assertNotEquals(actual, expected[, extra_msg])
值断言
-- 应用lua的规则,除了false和nil以外的值都认为是true,可以通过断言。
assertEvalToTrue(value[, extra_msg])
-- 只有false和nil才能通过断言,其他值均不能通过断言。
只有false和nil才能通过断言,其他值均不能通过断言
-- 只有true才能通过断言
assertTrue(value[, extra_msg])
-- 只有false才能通过断言
assertFalse(value[, extra_msg])
-- 只有nil才能通过断言
assertNil(value[, extra_msg])
-- 非nil的值都可以通过断言
assertNotNil(value[, extra_msg])
-- 只有actual和expected指向同一个对象才能通过断言,对于基本类型string、numbers、boolean、nil,等效于assertEquals
assertIs(actual, expected[, extra_msg])
-- actual和expected指向不同对象才能通过断言
assertNotIs(actual, expected[, extra_msg])
类型断言
-- 变量类型是number才可通过断言
assertIsNumber(value[, extra_msg])
-- 类型为string才可通过断言
assertIsString(value[, extra_msg])
-- 类型为table才可通过断言
assertIsTable(value[, extra_msg])
-- 类型为boolean才可通过断言
assertIsBoolean(value[, extra_msg])
-- 类型为nil才可通过断言
assertIsNil(value[, extra_msg])
-- 类型为function才可通过断言
assertIsFunction(value[, extra_msg])
-- 类型为userdata才可通过断言
assertIsUserdata(value[, extra_msg])
-- 类型为协程才可通过断言
assertIsCoroutine(value[, extra_msg])
-- 类型为线程才可通过断言
assertIsThread(value[, extra_msg])
单元测试范围
单元测试专注于验证软件中最小的可测试部分——功能单元的正确性。其核心目标是对功能单元的外部接口进行测试,以确保这些接口的稳定性和可靠性。由于功能单元内部调用的函数可能会因为各种原因(如代码重构或功能优化)而发生变化,因此这些内部函数不需要直接进行单元测试。
在Lua模块中,单元测试的对象是那些可供组件内其他Lua模块访问的函数。这些函数通常是通过模块的return
语句返回的,或者是定义在返回的table
内的函数。如果一个函数是局部的(local
),并且没有通过return
语句导出,那么这个局部函数通常不需要单独进行单元测试。这类函数的测试通常通过其他对外暴露的函数来间接完成。然而,如果某个局部函数通过return
语句被导出,那么它也需要被纳入单元测试的范围。
测试代码编写
UT是针对接口层面的测试用例,其测试代码位于组件仓的test/unit
目录下。开发人员在编写单元测试代码时,需根据测试模块在test/unit
目录下定义对应的测试用例文件。测试用例文件编写规则如下:
根据测试功能定义对应的测试文件:测试文件名需以
test
开头,后面跟上该文件具体测试功能。如test_operation.lua
在测试文件中需要定义一个全局表对象,用来承载具体的测试函数,测试函数名同样需要以
test
开头。示例如下:被测文件
operation.lua
: 作为源码文件,存放在src/lualib
目录下lua-- 该文件为业务功能实现文件,实现了运算方法 local calculator = {} function calculator:multiply(v1, v2) return v1 * v2 end function calculator:divide(v1, v2) if v2 == 0 then error("Division by zero") end return v1 / v2 end return calculator
对应测试用例文件
test_operation.lua
: 作为UT文件,存放在test/unit
目录下lua-- 导入被测模块 local calculator = require 'operation' -- 导入LuaUnit框架 local lu = require 'luaunit' -- 编写测试用例 test_calculator = {} -- 测试 function test_calculator:test_multiply() local re = calculator:multiply(0, 1) lu.assertEquals(re, 1) end function test_calculator:test_divide() local re = calculator:divide(0, 1) lu.assertEquals(re, 0) re = calculator:divide(1, 1) lu.assertEquals(re, 1) local ok = pcall(calculator.divide, 1, 0) lu.assertEquals(ok, false) end
openUBMC中,组件的单元测试是直接执行lua脚本,入口文件为test/unit
目录下的test.lua
。该文件的主要动作包含:加载配置文件、导入依赖库、导入所有测试模块(编写好的测试用例文件或目录)、通过luaunit启动测试。test.lua
中的大部分代码是固定的,无需修改。一般情况下,只需要使用require
将测试模块导入:
require 'test_operation'
完整的test.lua
文件示例如下:
loadfile(os.getenv('CONFIG_FILE'), 't', {package = package, os = os})()
local lu = require('luaunit')
local utils = require 'utils.core'
local logging = require 'mc.logging'
local current_file_dir = debug.getinfo(1).source:match('@?(.*)/')
utils.chdir(current_file_dir)
logging:setPrint(nil)
logging:setLevel(logging.INFO)
-- 导入测试模块
require 'test_operation'
-- 启动LuaUnit
os.exit(lu.LuaUnit.run())
在单元测试的入口文件test.lua
中导入测试模块之后,在组件仓路径下执行单元测试命令:
bingo test -ut
执行此命令会自动执行单元测试入口文件test.lua
。根据test.lua
文件,LuaUnit
框架会扫描导入的测试模块中所有的test
或Test
开头的变量(函数或表)来运行。
由于单元测试只关注功能测试,通过直接执行Lua脚本执行测试方法。对于需要调用外部组件接口的函数方法,单元测试无法调用到外部组件接口。面对此种情况,开发者可以通过mock
打桩的方式模拟接口调用。
简单来说,单元测试的实现流程如下:
确定测试范围:明确待测
lua
模块对外可访问的函数。对于新模块,在
test/unit
目录下创建test_xxx.lua
文件(xxx即为业务代码文件的名称),编写相应开发者测试代码: 单元测试文件的编写格式如下: 1)导入被测模块。 2)创建测试table
(同一类的用例需要通过一个table
整合在一起,组成一个测试套,即一组相似的测试用例)。 3)编写测试函数主体,请注意测试用例应包含正常业务流,并考虑关键异常逻辑、边界值等场景。测试覆盖率应尽可能完整。在单元测试入口脚本
test.lua
中加载测试文件,例如:luarequire 'test_operation'
在组件路径下运行测试命令执行组件的单元测试。
shellbingo test -ut
测试覆盖率
openUBMC的工程工具bingo
提供了单元测试覆盖率统计。覆盖率统计框架luacov
自动统计单元测试用例的覆盖率。此框架和luaunit
开发框架一样,也已转移至开发工具,不需要开发人员自行配置。 开发者在编写完单元测试和业务代码后,可在组件目录下执行以下命令统计测试覆盖率:
bingo test -ut -cov
此命令会自动检测UT代码覆盖率,并在temp/coverage
路径下生成覆盖率可视化文件luacov.report.html
。同时,开发者也可以通过控制台查看代码覆盖率统计结果。统计结果示例如下:
行覆盖率: 18.7%, 命中: 346 行, 未命中: 1509 行
覆盖率报告 luacov.report.html 保存到 /new_app/temp/coverage
Dt 报告结果输出到 /new_app/temp/coverage/dt_result.json
增量总覆盖:100.0%, c代码覆盖: 100.0%, lua代码覆盖: 100.0%
增量覆盖率报告保存到 dt_result.json
================================ 构建菜单 ================================
单元测试(ut): True
集成测试(it): False
覆盖率: True
地址消毒: False
============================================================================
组件的覆盖率统计范围包括组件代码根目录src
下的所有Lua代码的覆盖率。
开发者在进行单元测试代码编写时,应尽可能提高测试用例的覆盖率,并充分考虑测试用例的边界值。
集成测试
单元测试是对组件内功能函数的测试,而集成测试主要关注本组件与其他组件的接口可靠性。集成测试通过D-Bus
对外提供的接口需要开展集成测试,确保组件对外接口稳定。以组件my_app
为例,在集成测试中,配置文件test/integration/test_my_app.conf
(此配置文件名格式为以test_
为头,后面跟组件名)通常用于定义Skynet的环境变量、引用集成测试所依赖的组件。而以test/integration/test_my_app.lua
(此文件名格式为以test_
为头,后面跟组件名)作为集成测试的入口运行文件。开发人员可在此文件中编写集成测试代码或引用集成测试文件。
test_new_app.config
文件作为组件集成测试的入口配置文件,集成测试命令bingo test -it
通过该配置文件的定义执行集成测试。test_new_app.config
简要介绍如下:
-- 引入/root/BMC/new_app/temp/opt/bmc/libmc/config.cfg文件
include("$CONFIG_FILE")
-- 初始化集成测试目录
config:init_integration_test_dirs()
-- 定义集成测试的入口文件
config:set_start("test_new_app")
-- 定义集成测试相关的组件,注意此组件必须在service.json中定义test依赖
config:include_app('new_app')
config:done()
-- 定义SkyNet的变量,这些变量可在集成测试的lua文件中使用
TEST_DATA_DIR = 'test/integration/.test_temp_data/'
test_apps_root = 'test/integration/apps/'
在test_new_app.config
配置文件中,config:set_start("test_new_app")
定义了集成测试的入口文件为test_new_app.lua
文件。test_new_app.lua
简要介绍如下:
-- 引用集成测试所需的服务
local skynet = require 'skynet'
require 'skynet.manager'
local log = require 'mc.logging'
local utils = require 'mc.utils'
local test_common = require 'test_common.utils'
-- 集成测试准备阶段,创建临时目录用于存放集成测试过程中需要的文件
local function prepare_test_data()
local test_data_dir = skynet.getenv('TEST_DATA_DIR')
-- 变量test_data_dir在test_xxx.config中已定义
os.execute('mkdir -p ' .. test_data_dir)
os.execute('mkdir -p ' .. '/tmp/test_dump')
end
-- 清除测试数据,避免临时目录存在数据影响集成测试的执行
local function clear_test_data(exit_test)
log:info('clear test data')
local test_data_dir = skynet.getenv('TEST_DATA_DIR')
if not exit_test then
return utils.remove_file(test_data_dir)
end
skynet.timeout(0, function()
skynet.sleep(20)
skynet.abort()
utils.remove_file(test_data_dir)
utils.remove_file('/tmp/test_dump')
end)
end
-- 集成测试的业务测试代码,开发人员可在此部分编写相关业务测试代码
local function test_new_app()
log:info('================ test start ================')
-- todo
log:info('================ test complete ================')
end
-- 集成测试的入口函数
skynet.start(function()
clear_test_data(false)
prepare_test_data()
test_common.dbus_launch()
skynet.uniqueservice('main')
skynet.fork(function()
local ok, err = pcall(test_new_app)
clear_test_data(true)
if not ok then
error(err)
end
end)
end)
简单来说,集成测试的实现流程如下:
确定测试范围:明确组件对外
rpc
接口。对于新模块,集成测试可能存在多种场景,每种场景的配置文件和测试脚本均不同,此时可在
test/integration
目录下创建不同的子目录,来管理不同的集成测试场景。在
service.json
文件中定义集成测试需要依赖的组件。在集成测试配置文件
test_xxx.conf
和集成测试skynet启动函数所在脚本文件test_xxx.lua
编写相应代码,调用测试函数。运行测试命令执行组件的集成测试
shellbingo test -it