前文已介绍完框架的搭建过程,本文接着讨论这个框架的运行机制。
1. 环境
首先定义框架的运行环境,也可以理解为运行测试的上下文:
TCR_OK = 0 -- 测试通过
TCR_FAILED = 1 -- 测试没通过
TCR_ERROR = 2 -- 被测试对象有lua错误,没完成测试
TCR_EXCEPTION = 3 -- 用例本身的错误,没成功执行测试
local TC_OK = 0 -- 继续运行
local TC_DELAY = 1 -- 延时运行
local TC_SUSPEND = 2 -- 挂起
local TC_OVER = 3 -- 运行完
TC_Context =
{
-- 执行的用例节点相关的信息
root = nil, -- 测试的根节点
start = nil, -- 测试的起始节点
sindexs = {}, -- 起始节点的索引树
co = nil, -- lua协程
-- 运行状态记录:
flag = TC_DELAY, -- 状态标记,见TC_***定义
delay = 0, -- 延迟多久继续执行,单位ms
cindexs = {}, -- 待执行的用例节点的索引树
}
简单的Get/Set、Is函数不列举了。这里仅列出最关键的接口。
-- 上下文中delay不为0,表示需要暂停执行当前用例,延迟一段时间后恢复执行该用例
function TC_Context:IsPause()
return self.delay > 0
end
-- 因为是树形结构,在遍历树的时候,免不了压栈出栈操作
function TC_Context:PushCase(index)
table.insert(self.cindexs, index)
end
function TC_Context:PopCase()
local index = -1
local n = table.getn(self.cindexs)
if n > 0 then
index = self.cindexs[n]
table.remove(self.cindexs)
end
return index
end
2. 运行
接下来看看,整个框架是怎么跑起来的。
一个测试节点的运行算法极其简单:
function Test_Node:Execute()
if not self:IsEnable() then
return true -- 本节点被禁用,返回true,表示已运行完
end
if self.type == TNType_Suit then
return self:ExecuteSuite()
else
return self:ExecuteCase()
end
end
Test_Node:Execute
返回结果表明本测试节点是否运行完,后文会详细解释这个返回值。再看套件节点是怎么运行的:
function Test_Node:ExecuteSuite()
local index = TC_Context:PopCase() -- 弹出要执行的节点索引
if index < 1 then
index = 1 -- 没有指定,就从第一个子节点开始
end
for n = index, table.getn(self.cases) do
if not self.cases[n]:Execute() then
TC_Context:PushCase(self:GetIndex()) -- 有子节点没执行完,那么本节点就不算执行完
return false
end
end
return true
end
Test_Node:Execute
和Test_Node:RunSuite
这两个核心函数表面看起来是个“深度优先遍历”的递归函数。用例树上,叶节点是用例节点(其它节点都是套件节点),只有用例才是可执行的,因此每一次从根节点进入时,都要(递归)找到目标用例节点以执行用例。我们再来看看用例节点的运行函数:
function Test_Node:ExecuteCase()
if TC_Context:IsPause() then
TC_Context:PushCase(self:GetIndex()) -- 本节点还没执行完
return false
end
-- 这里可以插入代码,将运行状态信息报告给用户界面
-- 用例函数在协程中执行,便于支持Sleep功能
if not TC_Context.co then
TC_Context.co = coroutine.create(self:GetFunc())
end
local suspended = false
local rt, res = coroutine.resume(TC_Context.co)
if rt then
if coroutine.status(TC_Context.co) == "suspended" then
if not res then
TC_Context.flag = TC_DELAY -- 用例报错,延时继续
TC_Context:SetDelay(10) -- 没通过测试,延时10ms
TC_Context.co = nil -- 用例测试没通过,释放线程,继续执行后面的用例
else
suspended = true
TC_Context.flag = TC_SUSPEND -- 用例挂起,延时继续
TC_Context:SetDelay(res) -- 挂起指定的毫秒数
end
else
TC_Context.flag = TC_OK -- 用例通过,怎么延时由开发者在用户接口中指定
TC_Context.co = nil -- 用例测试通过,释放线程,继续执行后面的用例
end
else -- 报错了
TC_Context.flag = TC_DELAY -- 用例报错,延时继续
TC_Context:SetDelay(10) -- 延时10ms
TC_Context.co = nil -- 释放线程,继续执行后面的用例
end
-- 这里可以插入代码,将运行状态信息报告给用户界面
if suspended then -- 挂起了
TC_Context:PushCase(self:GetIndex()) -- 本节点还没执行完
return false
end
return true -- 本节点已执行完
end
以上是3个核心函数,现在看看怎么把它们组合起来:
-- 本函数是用户接口
function GB_StartRun()
if TC_Context.root == nil or TC_Context.start == nil then
return
end
TC_Context:Init() -- 初始化上下文
GB_Run()
end
-- 本函数是框架内部的运行入口
function GB_Run()
if TC_Context.start == nil then
return
end
TC_Context:ResetDelay() -- 复位延时
TC_Context:PopCase() -- 把start的索引弹出来
TC_Context.start:Execute() -- 从起始节点开始执行
if not TC_Context:HasCases() then
TC_Context:Over()
return
end
if TC_Context:IsPause() then
SetTimer(TC_Context:GetDelay(), 1, "GB_Run") -- 设置一个单次定时器,延时再次执行本函数
end
end
GB_Run
函数是定时执行的,具体的定时机制由各软件产品自行决定。在该函数的尾部检查上下文中的delay字段,如果不为0,比如3000,则延时3秒后继续执行GB_Run
。下面详细解释一下这个函数的工作机制:
1. 该函数首先复位环境中的延时,在执行用例的时候这个延时字段可能会被修改,因为用例开发者比较清楚用例在干什么,他可能需要自行决定一个用例执行完之后需要多久再执行下一个用例。
以一个MMORPG游戏中商城购买宝石的自动化测试为例来说,假如在用例中发起一次购买宝石的请求,然后校验包裹中是否有期望的宝石存在,以及银两是否如数扣除。这个购买请求通过tcp连接发到服务器,在经过:校验银两,校验包裹、扣除银两、添加宝石到包裹,同步包裹物品和银两到客户端等一系列过程才算完成整个购买操作。而对于用例来说,很显然它发送完购买请求之后,是不能立即进行校验的,它要等整个购买过程完成之后才能进行校验,而这个过程需要耗费一定的时间完成,具体多长时间,和具体产品有关,由开发人员自行决定,他可以在用例中发起购买请求之后Sleep几秒,几秒之后继续执行后续的校验。注意:这里的Sleep不是说将框架挂起,因为本框架假设是被集成进具体的软件产品中的(比如MMORPG的客户端程序中),如果框架在独立的线程中执行,可能还可以挂起而并不影响客户端的正常运行,但仅仅是想进行自动化测试而要求被测试软件改变线程模型,对正常业务数据或逻辑增加大量无谓的锁,就有点削足适履的味道了。因此本框架更进一步假设是被集成进软件产品中,且和正常的业务逻辑代码同等地位,甚至位于同一线程中(lua版本可以使用协程轻松做到Sleep且不影响业务流程)。有朋友可能会问,如果框架不集成进具体的产品中,而是独立做成一个测试工具呢?那当然可以随意挂起了,只是这就不是一个软件测试框架了,而是一个具体的产品(就像外挂一样),开发量会成倍增长。如果框架集成进被测试的软件中,可以最大限度的复用被测试软件的代码,大幅降低开发工作量。
2. 在运行测试节点之前,要先把环境中的cindexs表里的当前节点弹出(在GB_Run
中的当前节点就是start节点)。每次执行完GB_Run
时,cindexs表以栈的形式存储下次待执行节点的索引序列,栈底是用例节点的索引,栈顶是start节点的索引。进入GB_Run
函数首先弹出自己的索引,然后由start节点的Execute开始,一层层剥离,直到用例节点(此时cindexs表刚好被清空),开始执行Test_Node:ExecuteCase
,形象一点说,就像从树根开始找到某片树叶一样。
3. 最后一个用例执行完之后,TC_Context:HasCases
返回false,整棵树就执行完了。
4. 一个用例执行完,返回true,则继续执行后面的用例:如果该用例所在的套件下还有下一个兄弟用例,则继续执行下一个兄弟用例;否则找到下一个套件中的第一个用例继续执行;如果用例执行完返回false,则向环境cindexs表中压入该用例索引,然后一层层退出,每退出一层,压入该层节点的索引,直到start节点;在最后退出之前,根据环境中的延时字段,设置定时等待下一次的迭代。循环往复,直到运行完整棵树上的所有用例。
到这里,框架的运行机制讨论的差不多了,下一篇的主题是用户接口,讨论在这个框架上开发测试用例时用到的一些方法。