手撸软件测试框架——lua版(二)

1. 基础组件

  首先我们来实现框架中的基础组件:“用例”。用例是一个可执行的对象,负责具体业务的测试:


local TNType_Case  = 1  -- 用例
local TNType_Suite = 2  -- 套件
-----------------------------------------------------------------
-- 测试节点
local Test_Node =
{
    type    = 0,    -- 类型:1:用例,2:套件
    index   = 0,    -- 用例索引
    parent  = nil,  -- 父节点
    state   = 1,    -- 状态,0:禁用;1:启用;-1:未定义
    name    = "",   -- 用例名或套件名
    author  = "",   -- 作者名
    desc    = "",   -- 描述
    -- 以下是套件独有的字段:
    -- cases = {},  -- 套件包含的子套件或用例
}

  lua里面没有类,“只有”表,你可以单独为套件创建一个表,比如Test_Suit:

-- 测试套件
local Test_Suit =
{
    type    = 0,    -- 类型:1:用例,2:套件
    index   = 0,    -- 套件索引
    parent  = nil,  -- 父节点
    state   = 1,    -- 状态,0:禁用;1:启用;-1:未定义
    name    = "",   -- 套件名
    author  = "",   -- 作者名
    desc    = "",   -- 描述
    cases   = {},   -- 套件包含的子套件或用例
}

但这样实现,不是很lua,简单点设计,就用Test_Node统一表示“用例”和“套件”(可以称作节点或测试节点),你已经看到了,该表名叫Test_Node,而不是Test_Case,且第一个字段type,也表达出这个思路。
  这个表是测试节点的基类,我们需要在这个基类上创建各种不同的“用例”或“套件”实例对象:

-- 生成测试节点
function Test_Node:new(o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end

  套件是可以添加子套件或用例的,所以:

function Test_Node:AddTest(child)
    for _, tnode in ipairs(self.cases) do
        if tnode :GetName() == child:GetName() then
            return -- 不能创建同名用例
        end
    end
    
    table.insert(self.cases, child)
    child:SetParent(self)
end

  限于篇幅,各字段对应的Get/Set、Is等简单接口就不列举了。

  测试节点可以禁用/启用,单独列出CanEnableEnableDisable

-- 只有节点没有被启用,且父节点被启用(或不存在父节点)的情况下,本节点才能被启用
function Test_Node:CanEnable()
    return (self:IsDisable() and (self.parent == nil or not self.parent:IsDisable()))
end

function Test_Node:Enable()
    if not self:IsDisable() then
        return -- 本节点本就没有被禁用,直接返回
    end
    
    local p = self:GetParent()
    if p and p:IsDisable() then
        return -- 父节点被禁用,本节点不能被启用
    end
    
    self.state = 1 -- 设置为启用状态
    
    if self:IsSuite() then -- 如果本节点是套件,则递归启用其下所有节点
        for _, tnode in ipairs(self.cases) do
            tnode:Enable()
        end
    end
end

function Test_Node:Disable()
    if not self:IsEnable() then
        return -- 本节点本就被禁用,直接返回
    end
    
    self.state = 0 -- 设置为禁用状态
    
    if self:IsSuite() then -- 如果本节点是套件,则递归禁用其下所有节点
        for _, tnode in ipairs(self.cases) do
            tnode:Disable()
        end
    end
end

  套件需要一个返回指定索引的子套件或用例的接口:

function Test_Node:GetChild(index)
    if self.type == TNType_Case then
        return nil -- 用例没有子节点
    else
        return self.cases[index]
    end
end

  取一个节点下所有用例的总数:

function Test_Node:GetCaseSum()
    if self.type == TNType_Case then
        return 1
    else
        local sum = 0
        for _, tnode in ipairs(self.cases) do
            sum = sum + tnode:GetCaseSum()
        end
        return sum
    end
end

  取一个用例对应的执行函数:

function Test_Node:GetFunc()
    local casefun = _G[self:GetName()]
    if casefun and type(casefun) == "function" then
        return casefun
    end
    return nil
end

  现在,为所有用例节点分配索引:

-- 因为注册用例的顺序不可控,不同部门,不同开发人员在不同的文件中开发用例,而文件加载
-- 顺序无法控制,因此在注册用例的时候不能分配索引,只有等用例树构建完整之后,才能分配
-- index:分配给本用例节点的索引
function Test_Node:AssignIndex(index)
    self.index = index
    if self.type ~= TNType_Case then
        for i, tnode in ipairs(self.cases) do
            tnode:AssignIndex(i)
        end
    else
        -- 因为用例对应的函数可能会在注册用例之后才加载,所以,须等所有用例加载完之后才能检查是有用例
        -- 未定义执行函数,而本函数正是在这个时刻执行,所以在这里检查未定义情况,如果发现有用例未定义
        -- 执行函数,即向上回溯,一直到发现任意一个节点已被标记为未定义,则不用再回溯了。
        if self:GetFunc() == nil then -- 用例的执行函数未定义
            local tr = self
            while tr and tr.state ~= -1 do
                tr.state = -1 -- 置为未定义
                tr = tr.parent
            end
        end
    end
end

2. 注册

  基础组件已经建好,现在我们要开始实现注册机制,将基础组件组织起来形成一个完整的框架。首先,定义一个工厂类,工厂类负责创建对应的套件或者用例:

local TC_Factory =
{
    type        = 0,   -- 0:套件工厂;1:用例工厂
    name        = "",  -- 工厂节点名字
    -- author   = "",  -- 作者名,用例工厂才有的
    -- desc     = "",  -- 描述,用例工厂才有的
    -- factorys = {},  -- 子用例工厂,套件工厂才有的
}

function TC_Factory:new(o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end

-- 工厂池,简单起见,以工厂名为key,所以不能出现相同名字的工厂,不过这个限制
-- 很容易解除掉!比如,可以把自身的名字和所有祖先节点的名字串起来作为key。
local TC_Factorys =
{
    -- ['name'] = {}, -- 用例工厂
}

  接着,我们实现注册函数:

----------------------------------------------------------
-- 注册工厂
-- ftype:用例工厂类型
-- name:用例工厂名
----------------------------------------------------------
function TC_RegistryFactory(ftype, name)
    local tnf = TC_Factorys[name]
    if tnf == nil then
        tnf = TC_Factory:new{ type = ftype, name = name}
        if ftype ~= TNType_Case then
            tnf.factorys = {}
        end
        TC_Factorys[name] = tnf
    end
    
    return tnf
end

-------------------------------------------------------------------
-- 注册套件工厂
-- parent:父套件名字
-- child:子套件名字
-------------------------------------------------------------------
function TC_RegistrySubSuite(parent, child)
	-- 注册套件工厂时,不存在的话就新建一个套件工厂,这样方便开发
    local ptnf = TC_RegistryFactory(TNType_Suite, parent)
    local tnf = TC_RegistryFactory(TNType_Suite, child)
    
    for _, x in ipairs(ptnf.factorys) do
        if x.name == child then
            return
        end
    end
    
    table.insert(ptnf.factorys, tnf)
end

------------------------------------------------------
-- 注册用例工厂
-- parent:套件名字
-- func:用例名,通常是可执行的lua函数的名字
-- author:用例作者
-- desc:用例描述
------------------------------------------------------
function TC_RegistryCase(parent, func, author, desc)
    local ptnf = TC_Factorys[parent]
    if ptnf == nil then
        return -- 注册用例工厂,前提是其父节点工厂必须存在
    end
    
    for _, x in ipairs(ptnf.factorys) do
        if x.name == func then
            return -- 不能有同名的兄弟节点
        end
    end
        
    local tcf = TC_RegistryFactory(TNType_Case, func)
    tcf.author = author -- 用例工厂要有作者名
    tcf.desc = desc -- 用例工厂要有描述
    
    table.insert(ptnf.factorys, tcf)
end

  注册接口准备好了,我们用这几个接口来构建手撸软件测试框架——lua版(一)中的“测试用例树”,代码如下:

TC_RegistryFactory(TNType_Suite, "【通天传】产品测试") -- 注册根

TC_RegistrySubSuite("开发部用例", "性能测试用例")
TC_RegistrySubSuite("开发部用例", "踩踏用例")
TC_RegistrySubSuite("开发部用例", "内存泄露用例")

TC_RegistrySubSuite("性能测试用例", "诛仙阵用例", "诛仙阵")
TC_RegistrySubSuite("性能测试用例", "万寿山用例", "万寿山")
TC_RegistrySubSuite("内存泄露用例", "传送循环", "传送")
TC_RegistrySubSuite("内存泄露用例", "上下线循环", "上下线")

TC_RegistrySubSuite("测试部用例", "任务系统专题用例")
TC_RegistrySubSuite("测试部用例", "玩法系统专题用例")
TC_RegistrySubSuite("测试部用例", "客户端协议测试")
TC_RegistrySubSuite("测试部用例", "系统测试用例")

TC_RegistrySubSuite("任务系统专题用例", "帮派押镖测试用例")
TC_RegistrySubSuite("任务系统专题用例", "跑环测试用例")
TC_RegistrySubSuite("任务系统专题用例", "日常任务测试用例")
TC_RegistrySubSuite("任务系统专题用例", "地宫寻宝用例")
TC_RegistrySubSuite("任务系统专题用例", "兜率宫炼丹用例")

TC_RegistrySubSuite("玩法系统专题用例", "女儿国用例")
TC_RegistrySubSuite("玩法系统专题用例", "幽灵地府用例")
TC_RegistrySubSuite("玩法系统专题用例", "道场一条龙用例")
TC_RegistrySubSuite("玩法系统专题用例", "广寒宫用例")
TC_RegistrySubSuite("玩法系统专题用例", "大闹天宫用例")

TC_RegistrySubSuite("系统测试用例", "仙翼用例")
TC_RegistrySubSuite("系统测试用例", "法宝用例")
TC_RegistrySubSuite("系统测试用例", "交易用例")
TC_RegistrySubSuite("系统测试用例", "装备用例")
TC_RegistrySubSuite("系统测试用例", "帮派用例")
TC_RegistrySubSuite("系统测试用例", "仙术用例")
TC_RegistrySubSuite("系统测试用例", "姻缘用例")

TC_RegistryCase("诛仙阵用例", "GB_LoginRobot", "陈龚鹏")
TC_RegistryCase("诛仙阵用例", "GB_MoveTogether", "陈龚鹏")
TC_RegistryCase("诛仙阵用例", "GB_CallQunXian", "陈龚鹏")
TC_RegistryCase("诛仙阵用例", "GB_AutoPk", "陈龚鹏")

  以上只是代码示例,在实际应用中,不同的部门,模块,开发人员可能在不同的文件中编写测试用例。一般地,在注册“用例”工厂时,要求其所属的套件工厂已注册是合理的;但是套件工厂有点不一样,不同的人可能会在不同的文件中向同一个套件注册不同的子套件,由于文件加载顺序不可控,所以必须保证套件工厂的注册不受文件加载顺序的影响,因此在用TC_RegistrySubSuite注册时,如果parent套件工厂没有注册,则需要自动注册一下。

3. 构建用例树

  上述一系列注册的过程只是告诉框架,按着指定的层级关系建立起一棵树,这棵树上的每个节点还不是测试节点(Test_Node),而是负责创建Test_Node对象的工厂。我们需要这棵工厂树生产我们需要的产品树,也就是用例树:

-- 构建用例(树),该接口主要用于初始化时通过工厂树生成用例树
function TC_Factory:MakeCase()
    local tnode = Test_Node:new{ type = self.type, name = self.name }
    if self.type == TNType_Case then
        tnode.desc = self.desc
        tnode.author = self.author
    else
        tnode.cases = {}
        for _, tnf in ipairs(self.factorys) do
            tnode:AddTest(tnf:MakeCase()) -- 递归生成子用例树
        end
    end
    
    return tnf
end

-- 重载用例(树),该接口主要用于动态加载新注册的用例工厂,但不想重启进程,而是通过刷lua脚本加载
-- tnode:用例节点
function TC_Factory:ReLoadCase(tnode)
    if self.type == TNType_Case then
        return -- 用例工厂只会生成一个用例,不会有两个以上的子节点
    end
    
    if self.name ~= tnode:GetName() then
        return -- 工厂和节点不匹配
    end
    
    for index, tnf in ipairs(self.factorys) do
        local snode = tnode:GetChild(index)
        if snode then
            if snode:GetName() == tnf:GetName() then
                tnf:ReLoadCase(snode)
            end
        else
            tnode:AddTest(tnf:MakeCase()) -- 新注册的用例
        end
    end
end

  一般在系统启动完成之后,才能构建整棵用例树,封装一个接口:

function GB_BuildCaseTree()
    local tnf = TC_Factorys["【通天传】产品测试"]
    if tnf == nil then
        return
    end
    
    if g_root then -- 追加
        tnf:ReLoadCase(g_root)
    else
        g_root = tnf:MakeCase()
    end
    -- 这里需要为所有用例分配一下序号
    g_root:AssignIndex(1)
end

  到这里,框架的核心部分已经完成。手撸软件测试框架——lua版(三)中继续讨论用例树的运行机制。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值