用FSM写Case,你会么?

时间:2022-04-27
本文章向大家介绍用FSM写Case,你会么?,主要内容包括1.引言、2.基于状态的测试、3.从状态机到测试用例、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

1.引言

腾讯测试工程师小新一是一名资深的安卓客户端测试工程师,对于安卓客户端的功能测试、自动化测试和性能测试方面都有着非常丰富的经验。最近小新一被通知负责某二手交易app的功能测试,在初步了解了该app后,小新一皱起了眉头。

该app虽然看起来功能简单,只是提供了一个买家和卖家的交易沟通平台,但是其中涉及到了多个实体的状态变迁,如果只是对于需求进行测试用例设计的话,很难保证所有的功能路径都被覆盖了,而且测试用例对于路径的覆盖无法区分优先级。这样的测试用例是远远不能保证到产品的质量的。

针对这个情况,小新一和测试分析小组负责人锅仔进行了一次深入的沟通,在听完小新一对于测试任务的描述后,锅仔提出了使用基于状态的测试方法来完成对于该app的测试。

那么什么是状态机呢?什么又是基于状态的测试呢?怎么使用基于状态的测试呢?基于状态的测试适用于什么情况呢?在使用状态机的时候需要注意哪些事项呢?如果你对这些问题还存有疑问,那么请看官继续往下看,和小新一一起,学习基于状态的测试方法。

2.基于状态的测试

2.1 定义

基于状态的测试是一种基于模型的测试方法,作为黑盒测试设计技术中的一种,常被用于事件驱动的系统中。基于状态的测试核心思路是通过遍历系统所有的状态转换迁移,来保证整个系统功能的正常。

2.2 状态机

顾名思义,基于状态机的测试,其核心模型就是状态机,也叫状态图。状态机的组成其实比较简单,要素大致有三个:输入,输出,还有状态。输入和输出比较容易理解,那么什么叫做状态呢?状态就是对象生命期中的条件或情况,在这种状态中,对象满足某种条件,执行某种活动,或者等待某种事件。

在基于状态的测试中,状态机的准确度直接决定了测试效果,所以状态机的绘制是非常重要的一环,我们可以通过以下四步来分析如何绘制状态机:

步骤一:列出研究对象拥有的各种状态

通过启发式的探索来发现系统的状态:

1)通过三个简单问题发现状态:有没有什么事情是我现在可以做但之前不可以做的?有没有什么事情是我现在不可以做但之前可以做的?我现在所采取的行动是否产生了和之前不同的结果?

2)留意用于描述正在发生事情的言辞,如“当……的时候”(While)、“当系统正在导入数据的时候……”、“当账户被冻结的时候……”

3) 每个状态都由事件所触发,认出状态可回过头找出触发事件,反之亦然

步骤二:列出状态之间的转换,确定引起各个转换的事件

在步骤一的基础上,考虑状态之间的事件。从测试的视角来看,引起状态转换的事件可以分为三种类型:

1)外部产生事件:来自于软件之外的任何事件,如用户操作

2)系统产生事件:软件自己产生的任何事件,如系统完成了某些后台活动而产生的结果

3)时间流逝:超时、计时事件(如After 3 sec)

步骤三:分析各个转换过程中发生的事情

转换代表了从一种状态到另一种状态的改变,当然也可以是自身到自身的。每个状态都可以指定三种可选的信息:

1)触发器:触发器对应事件

2)守卫:守卫是一个布尔表达示,事件发生时,守卫必须为真,转换才会执行

3)效果:效果是在转换过程中执行的行为(活动或交互)

步骤四:状态机Review

在上面三步做完之后,就形成了一个状态机。但是,状态机的正确性,完整性是否可以得到保证呢?状态机的绘制是否符合规范呢?这些都需要通过人工或者工具的方式进行review。

以腾讯地图App的收藏夹模块为例,我们尝试对其进行状态机建模。

1、 列举研究对象的各种状态。收藏夹功能模块包含的对象比较简单,就是收藏夹页,这个页面包含了以下六个状态:

1) 未登录/无数据态

2) 未登录/有数据态

3) 微信登录/同步态

4) 微信登录/未同步态

5) QQ登录/同步态

6) QQ登录/未同步态

2、 列举各个状态之间的转换,确定各个转换的事件。

从收藏夹需求中,我们不难得出收藏夹六个状态之间的转换关系如下:

1) 在状态1添加数据,进入状态2

2) 在状态2修改数据,保持状态2;

3) 在状态2将数据全部删除,进入状态1

4) 在状态1进行微信登录,进入状态3

5) 在状态1进行QQ登录,进入状态5

6) 在状态2进行微信登录,进入状态4

7) 在状态2进行QQ登录,进入状态6

…….

3、 在列举完所有状态转换事件之后,对各个事件以触发器/守卫/效果三个维度进行分析。

对于事件1,触发器为添加了收藏点或者常用地址,守卫为网络畅通,效果为在收藏夹页面添加了相应的收藏夹数据。

在上面三个步骤执行玩之后,我们可以得到收藏夹模块的状态图,如下所示:

图2.1 腾讯地图App收藏夹功能状态机

2.3 适用范围及注意事项

基于状态的测试适用于下面的情况:

1)需求或者设计中使用了状态机

2)功能中包含了实体的增删查改

3)功能属于事件驱动型,并且系统状态容易识别

4)在需要对关键模块/业务进入深入的测试的时候使用

状态机绘制的注意事项:

1)保证每个状态的单一性,但是也要谨慎选择状态集合,避免状态空间爆炸

2)选择合适的分析实体,使用层次化方法以管理复杂性

3)要关注可观察的行为而不是实现细节

4)借助工具思考(NMODEL,基于整数规划的覆盖方法,后面会讲到)

2.4 状态机绘制实例

2.4.1 功能需求描述

***是一款提供二手物品交易的app,交易模块是该app的重点功能。交易过程中,卖家可以发布自己的闲置物品,买家可以根据分类来搜索自己想要购买的物品,当看到自己心仪的物品后可以拍下,当然也可以直接和买家联系,当买家拍下后,卖家可以收到系统发来的IM消息通知,从而处理发货等流程,当然在买卖过程中如果发生退款退货等情况的时候,双方可以自行处理,如果一方不满意的话,可以发起申请,平台介入后会根据双方提交的证据在进行仲裁,仲裁的结束后,会将处理结果告知双方。

2.4.2 模块选择过程

一开始,我们认为在买卖过程中,我们只需要以“买家”和“卖家”作为两个元素来进行建模,就可以覆盖到所有的状态,因此我便画出了如下的状态转化图:

图2.2 卖家的订单状态图

图2.3 买家的订单状态图

然而发现,这其中有一个问题,就是根据这样的状态图去设计测试用例,设计出的都是针对一方的,而在实际的买卖过程中,只有一方的操作时无法完成整个交易的。另外还会有一个问题就是,有些异常的情况是无法覆盖到的。举个例子:比如买家从V2(待付款)到V3(代收货)这个过程中,如果卖家关闭了订单,会发生什么呢?

因此我觉得应该将买家和卖家放到一起,将他们的操作流程给串起来,于是我又画出了如下的状态图:

图2.4 初步融合起来的状态图

然而这个图还是存在问题,当然这个问题在于我对一开始分析对象划分的不够细,其实这里面,应该划分为“订单”,“物品”,“操作者”,这里操作者又分为“买家”“卖家”“系统”“管理员”,这里应该订单这个元素状态较多,并且贯穿于整个的交易流程中,因此我们就选择“订单”作为此次测试建模的一个对象。

2.4.3 状态机绘制

基于上面的分析,我们最终确定了状态机的对象是订单,于是我们对订单的状态进行了一个列举(从后台的代码中列举出所有订单的状态码),然后画出状态转化图:

图2.5 订单的状态图

3.从状态机到测试用例

在针对所测功能模块绘制完状态机后,下一步便是在状态机的基础上生成测试用例。由于状态机描述了系统状态的所有转换,所以在构造测试用例的时候,只要保证状态机中的状态转换均被覆盖了,就能保证功能的测试完整了。比较通用的方法是通过单一状态转换表和转换对,构造几条覆盖全部状态的路径,以这几条路径为基础,生成基础测试用例。

3.1 简单状态转换覆盖

对一个状态机进行全覆盖,最简单的方法就是取出所有状态转换的状态对,对其进行逐一覆盖,我们称这种测试用例生成方法为简单状态覆盖方法。

首先,我们根据已经生成的状态机,将所有状态两两组合,形成一个状态转换表。如下表3.1所示:

表3.1 辅助转化表

No.

Start

Event

End

1

V1

买家支付

V2

2

V2

买家发起退款

V3

3

V1

买家关闭

V8

4

V1

卖家关闭

V8

5

V1

买家超时未处理关闭

V8

6

V3

卖家同意退款

V9

n

V5

买家撤销

V4

如上表所示,根据转换表第一条,我们需要覆盖从订单初始化到待发货的状态转换,因此我们构造一条用例为:

1)订单创建成功后,买家付款,在卖家发货前,买家发起退款,卖家同意退款后,订单关闭

上面这个用例不仅覆盖了初始化订单到待发货的状态转换,同时也覆盖了V2到V3、V3到V9的转换,因此我们在辅助转换表中,将其标识,如下:

表3.2 辅助转化表覆盖标识图

No.

Start

Event

End

1

V1

买家支付

V2

2

V2

买家发起退款

V3

3

V1

买家关闭

V8

4

V1

卖家关闭

V8

5

V1

买家超时未处理关闭

V8

6

V3

卖家同意退款

V9

n

V5

买家撤销

V4

按照这种方法,我们依次对辅助状态转换表中的所有转换进行覆盖用例设计,最终形成全量的测试用例集合。

3.2 手工状态机路径覆盖生成方法

简单状态转换覆盖方法的原理不复杂,但是在具体生成测试用例的时候比较困难,而且容易产生很多冗余的测试用例。在简单状态转换覆盖方法的基础上,我们结合状态机的路径覆盖方法,将生成的覆盖路径转换成测试用例。

在表3.1的基础上,我们将所有的状态抽取出来生成转化对,也就是列出每一个状态的输入流和输出流,然后将输入流和输出流进行排列组合作为状态流,如下表:

表3.2.辅助转化表

这时候“状态流”开始,然后每个终点的字母可以看它是否还是其他“状态流”的起点,如果是那么就继续往下,直到把所有的路径都覆盖完。这里举一个例子:

这样我们就可以在图中将覆盖掉的路径颜色进行一个标识,这样直到我们将图中所有的路径都覆盖掉

这里我手动执行完成后,得到的所有路径为:

3.3 扩充用例

在上述过程中,我们对订单正常状态的覆盖已经达到了。但是在实际的使用过程中,仍然存在这样的问题:卖家操作导致订单状态改变,而此时买家还停留在之前的界面,没有刷新UI,此时操作的case。因此针对这类的case,又延伸出如下一些用例:

买家异常

订单状态

非买家操作导致订单状态变为

买家

V1

V8

d

V2

V4

i

V3

V4

l

V3

V9

l

V3

V4

n

V3

V9

n

V5

V6

r

V5

V11

r

V5

V9

r

V5

V6

s

V5

V11

s

V5

V9

s

V6

V9

ac

V6

V9

aa

V6

V9

z

卖家异常

V1

V2

b

V1

V8

b

V2

V3

e

V2

V3

j

V3

V2

m

V3

V2

p

V5

V4

t

V5

V4

x

V5

V4

y

V6

V4

w

V6

V5

w

V6

V11

w

4.基于NModel的状态机-测试用例转换方法

不管是简单状态转换覆盖,还是手工状态机路径覆盖方法,在对复杂的状态机进行测试用例转换的时候,都会遇到不小的困难。这个事情并不需要人工的分析过程,能不能让机器帮我们完成呢?答案是肯定的。

NModel(官方地址:http://nmodel.codeplex.com/)是基础状态测试中常用的一个工具,它可以在我们列出对象的状态和执行的动作之后,自动帮我们构建状态图,并且还可以生成用例。其模型创建的原理是:

1.程序是用来处理数据的,数据也可以称作状态(State);

2.用户通过程序提供的操作界面来处理数据,操作界面也可以称作动作(Action);

3.数据的更动又反过来影响一些动作是否可以执行。

首先第一步需要抽象状态,在代码中我们用enmu类型来表示:

public enum OrderNum { v1, v2, v3, v4, v5, v6,v7, v8, v9, v10, v11 }

public enum OrderDesc { 初始状态, 代发货, 发货前退款, 代收货, 发货后退款, 发货后拒绝退款, 确认收货, 支付前关闭, 支付后关闭, 完成评价, 平台介入 }

接着是模拟一个动作

         [Action]
          public static void 下单后买家关闭()     
           {
             BookMarkShow.SetOrderState(OrderNum.v8);
           }
           publicstaticbool下单后买家关闭Enabled()
            {
               return (WebSiteModel.ordernum == OrderNum.v1);
           }

标注了[Action]的函数,就是抽象出来的程序所支持的动作,例如Logout;而在动作函数名后面加上Enabled的函数,是NModel用来判定指定的动作是否可以执行。

当我们列出所有的动作之后,可以使用如下命令来生成状态图:

mpv.exe /r:caseBookMarks.dllBookMarks.WebSiteModel.CreateLoginModel

查看模型没有问题后,我们就可以运行如下命令来生成用例了:otg.exe/r:caseBookMarks.dll BookMarks.WebSiteModel.CreateLoginModel/file:TestSuit.txt

TestSuite(
    TestCase(
        买家支付(),
        发货前卖家关闭订单()
    ),
    TestCase(
        买家支付(),
        发货前买家发起退款(),
        卖家拒绝发货前退款(),
        买家确认收货()
    ),
    TestCase(
        买家支付(),
        卖家发货(),
        发货后买家发起退款(),
        卖家拒绝了发货后退款(),
        买家修改发货后申请内容(),
        卖家超时未处理发货后退款申请()
    ),
    TestCase(
        买家支付(),
        发货前买家发起退款(),
        卖家同意发货前退款申请()
    ),
    TestCase(
        买家支付(),
        发货前买家发起退款(),
        买家撤销发货前退款申请(),
        发货前买家发起退款(),
        买家编辑发货前申请退款(),
        卖家超时未处理发货前退款申请()
    ),
    TestCase(
        下单后卖家超时未处理订单关闭()
    ),
    TestCase(
        买家支付(),
        卖家发货(),
        发货后买家发起退款(),
        卖家申诉(),
        仲裁结束()
    ),
    TestCase(
        下单后卖家关闭()
    ),
    TestCase(
        买家支付(),
        卖家发货(),
        发货后买家发起退款(),
        卖家拒绝了发货后退款(),
        拒绝退款后买家撤销申请(),
        发货后买家发起退款(),
        买家编辑发货后退款申请(),
        买家撤销发货后退款申请(),
        发货后买家发起退款(),
        卖家拒绝了发货后退款(),
        买家超时未处理拒绝退款(),
        发货后买家发起退款(),
        卖家拒绝了发货后退款(),
        卖家又同意退款申请()
    ),
    TestCase(
        下单后买家关闭()
    ),
    TestCase(
        买家支付(),
        卖家发货(),
        发货后买家发起退款(),
        卖家拒绝了发货后退款(),
        买家申诉()
    ),
    TestCase(
        买家支付(),
        卖家发货(),
        发货后买家发起退款(),
        卖家同意发货后退款申请()
    ),
    TestCase(
        买家支付(),
        卖家发货(),
        买家超时未处理收货(),
        买家完成评价()
    )
)