组件的独立测试
更新时间:2024/12/16
在Gitcode上查看源码

开发者在进行业务代码开发时,需要关注自己开发的功能是否正常运作。尽管这些功能可能只是整个系统中的一小部分,但它们仍然需要经过单元测试(UT)、集成测试(IT)以及一定程度的真实环境测试。这些测试的目的不仅是验证新开发的单元功能是否正确,还要确保这些新功能在集成过程中与周围接口的正确性,并且没有破坏其他功能。

开发者在进行开发者测试代码编写时,要以提高产品业务代码的可测试性,提升DT用例设计、DT用例实现与执行质量;优化DT工程编译、运行、调试加载时间和降低DT工程维护成本,从而提升DT效率,降低DT成本,提升开发生产率为目标。

对于openUBMC的各个组件来说,它们在开发阶段就具备了独立测试的能力。开发者进行的测试可以显著减少版本集成后的缺陷,并减少后期定位缺陷的成本。开发者编写的测试代码与业务代码具有同等的重要性。

当前openUBMC DT测试框架主要涵盖的场景包括单元测试和集成测试:

  1. 单元测试主要关注组件内函数可靠性,主要测试对象为组件内部各个子模块之间互相调用的函数。
  2. 集成测试主要关注本组件与其他组件的接口可靠性,不关注内部函数。

openUBMC的开发工具BMC Studio提供了开发者测试的CLI命令bingo test此命令的相关参数如下:

shell
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 -utbingo test -it分别单元测试和集成测试的运行命令,也是开发人员最常用的测试命令。

单元测试

单元测试(UT)又称为模块测试,是针对程序模块来进行正确性检验的测试工作。开发者需要为自己所要编写的代码和自动生成的代码编写测试用例,确保代码逻辑的正确性。

测试框架

openUBMC采用LuaUnit开源软件作为单元测试的基础测试框架。它提供了用例管理、调度和断言等功能,是一个功能全面、易于使用的单元测试框架。该框架会分析引用的文件中,所有以Test为开头的类,并依次调用以test_*命名的测试函数。目前,openUBMC已将luaunit转移至开发工具,在构建过程中会自动写入开发者测试依赖中,不需要开发人员自行配置。开发人员只需要在测试代码中导入LuaUnit,编写相关测试代码即可:

lua
luaunit = require('luaunit')

LuaUnit提供了assertXX()系列断言函数,比如assertNotNil(), assertEquals()等,可以提供更丰富的提示信息等。因此,在openUBM中,不直接使用assert()函数。下面简要介绍常用的几种断言函数:

相等性断言:

lua
-- 相等性断言。如果断言失败,将会打印extra_msg。对于table类型参数会进行更深层次的比较(元素个数、键、值比较)
assertEquals(actual, expected[, extra_msg])

-- 不等性断言。
assertNotEquals(actual, expected[, extra_msg])

值断言

lua
-- 应用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])

类型断言

lua
-- 变量类型是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将测试模块导入:

lua
require 'test_operation'

完整的test.lua文件示例如下:

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中导入测试模块之后,在组件仓路径下执行单元测试命令:

shell
bingo test -ut

执行此命令会自动执行单元测试入口文件test.lua。根据test.lua文件,LuaUnit框架会扫描导入的测试模块中所有的testTest开头的变量(函数或表)来运行。

由于单元测试只关注功能测试,通过直接执行Lua脚本执行测试方法。对于需要调用外部组件接口的函数方法,单元测试无法调用到外部组件接口。面对此种情况,开发者可以通过mock打桩的方式模拟接口调用。

简单来说,单元测试的实现流程如下:

  1. 确定测试范围:明确待测lua模块对外可访问的函数。

  2. 对于新模块,在test/unit目录下创建test_xxx.lua文件(xxx即为业务代码文件的名称),编写相应开发者测试代码: 单元测试文件的编写格式如下: 1)导入被测模块。 2)创建测试table(同一类的用例需要通过一个table整合在一起,组成一个测试套,即一组相似的测试用例)。 3)编写测试函数主体,请注意测试用例应包含正常业务流,并考虑关键异常逻辑、边界值等场景。测试覆盖率应尽可能完整。

  3. 在单元测试入口脚本test.lua中加载测试文件,例如:

    lua
    require 'test_operation'
  4. 在组件路径下运行测试命令执行组件的单元测试。

    shell
    bingo test -ut

测试覆盖率

openUBMC的工程工具bingo提供了单元测试覆盖率统计。覆盖率统计框架luacov自动统计单元测试用例的覆盖率。此框架和luaunit开发框架一样,也已转移至开发工具,不需要开发人员自行配置。 开发者在编写完单元测试和业务代码后,可在组件目录下执行以下命令统计测试覆盖率:

shell
bingo test -ut -cov

此命令会自动检测UT代码覆盖率,并在temp/coverage路径下生成覆盖率可视化文件luacov.report.html。同时,开发者也可以通过控制台查看代码覆盖率统计结果。统计结果示例如下:

shell
行覆盖率: 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简要介绍如下:

lua
-- 引入/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简要介绍如下:

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)

简单来说,集成测试的实现流程如下:

  1. 确定测试范围:明确组件对外rpc接口。

  2. 对于新模块,集成测试可能存在多种场景,每种场景的配置文件和测试脚本均不同,此时可在test/integration目录下创建不同的子目录,来管理不同的集成测试场景。

  3. service.json文件中定义集成测试需要依赖的组件。

  4. 在集成测试配置文件test_xxx.conf和集成测试skynet启动函数所在脚本文件test_xxx.lua编写相应代码,调用测试函数。

  5. 运行测试命令执行组件的集成测试

    shell
    bingo test -it