跳转至

21 低代码平台如何帮助应用做测试?被测功能的发现

你好,我是陈旭。

今天我们再次回到低代码平台的能力雷达图中来。

如果你已经淡忘了这个图的由来,那你可以回顾一下第15讲。简单地说,低代码平台不能只关注它的开发能力,还要关注应用开发的全生命周期,开发能力直接决定了低代码平台的综合能力上限,开发能力之外的其他能力决定了低代码平台的总体能力下限,这两种能力一样重要。这个雷达图从开发线(水平方向)到管理线(垂直方向)列举了低代码平台可以或者说需要具备的一些能力。

现在,让我们把眼光聚焦到水平线上右侧的自动化测试上。正如这一讲的标题所说,这里所说的自动化测试指的是应用的自动化测试,而非低代码平台自身的自动化测试。

通常,我们提及页面自动测试,它在研发流程中的位置大概是下面这样的。

先开发页面各个功能,大部分完成了后,再写测试用例。随后页面进入正常迭代,页面代码修改后,存量用例的代码需要同步对齐,同时也要补充一批新的用例。页面废弃功能的用例代码也需要同步清理。

注意,测试用例代码和正常功能代码开发&维护成本(包括技术难度和时间投入)基本一致。悲催的是,多数老板认为功能代码才是交付的特性,才是工作业绩,而不认可测试用例代码也是业绩的一部分,认为那只是特性团队为了让自己的日子好过而自行增加的“不必要”的工作量而已。在需求排期不紧急时,还能坚持编写和维护配套的测试用例,一旦满负荷时,第一个被砍掉的必然是测试用例,因此特性团队往往难以长期坚持做自动化测试。

即使能坚持编写测试用例,无需多久,累计的用例数量就会很可观,页面自动化测试用例的日常维护需求比 REST API 和单测用例代码的维护需求大得多。页面作为UI面向人类,是软件系统中需求最多变且细节最多的一部分,这就导致稍微有个风吹草动,页面的代码就要动,配套的用例也要同步修改。也就导致,测试用例的长期维护成本是非常大的,其维护工作量要显著大于API接口测试和单测代码。我常说的一句话是,页面自动化测试是笑着进去哭着出来,这就是原因。

回到主题上来。

低代码平台开发出来的App,如何做自动化测试呢?采用传统页面相似的方式来进行,可否?当然是可以的,但我们有更高效的方法。你想想,整个App都是低代码平台生成的,平台对App的结构和交互关系的理解,显然要比开发人员本身更深刻,因此,低代码平台显然有足够的数据可以来自动生成配套的自动化测试用例,还不止于此,更激动人心的是,低代码平台还有能力在无需让你干预的前提下,自动更新和维护测试用例。

接下来我将用三讲的篇幅,从原理到实现来说明低代码平台如何帮助应用实现零代码自动化测试。

什么是被测功能

这一切,要从这个最基本的问题开始,什么是被测功能?

采用传统方式开发出的软件,对功能的称呼可能各不相同,但基本上都是采用的语义化的方式来描述的,这些信息保存在离线Word文档中,或者在线文档库中,甚至啥文档都没有,只存在于某个人的大脑中。

虽然,部分比较规范的功能描述文档具有高度统一的文档结构,但其核心内容仍然是语义化的。显然,这些语义化文档从第一个字符开给就是为人类使用所准备的。看起来,似乎这些信息对我们今天要解决的问题毫无帮助。

开发人员通过阅读和理解文档中关于软件功能的描述,据此在低代码平台上,一步步将这些功能功能转为App的配置。反过来说,这些配置就是语义化文档的结构化表达,理解了这些结构化的配置信息,就基本等价于理解了语义化的文档,也就基本等价于得到了这个App所应该具有的各个功能。

前面的推理过程是有隐含的前提的,那就是人不会犯错,编写文档的人能准确描述出用户需求,开发人员可以准确理解并严格按照文档完成App的配置。不过,人类犯错这个因素,不在我们的考虑范围内,低代码平台输出的App为这一切的最终结论,只要这个App最终能通过客户的验收或者满足需求,那从逻辑上说,这个过程中的任何错误都不存在。

现在,我们已经知道可以从App的配置信息中来寻找被测功能了,但是一个App的配置信息非常非常多,哪些配置信息才能用于标志被测功能呢?这依然是一个复杂的问题。

我们采用归纳法,先从一个被测功能的实例开始,然后再尝试归纳出一个普遍的方法。下面这个动图演示的就是一个典型的被测功能。在查询条件栏中输入查询条件,点击查询按钮之后,页面向服务器发起了一个数据请求,收到数据之后,更新App中的图形,完成交互。

我们把动画演示的内容中的关键动作抽取出来,可以得到这个功能的主要流程。

嗯,这个功能的流程看起来简洁许多了,但是我们发现这个流程依然太具体了,并不能用于描述大多数的交互。假如我们把图形组件换成表格或者其他组件,那得到的这个功能流程,几乎和这个流程是一样的。

现在从别的角度思考:

  • 不是所有功能都是通过按钮来触发的,有可能是键盘事件,也可能是其他任何方式。
  • 正如动图中那个抽屉从关到开的过程一样,不是所有功能都需要进行网络I/O,这种只要简单修改浏览器内存中的某个状态值即可。

这些功能画出的流程显然与前面的流程会有很大差异,前面画出的流程依然不足够抽象,我们还需要进一步归纳。

试想一下:

  • 按钮点击也好,键盘按键也罢,自定义事件也行,我们可以归纳为功能的一种触发方式。
  • 组件发起网络I/O请求数据也好,抽屉的open属性从false变为true也罢,反正就是发生了一些事情。
  • 界面上图/表的数据更新了也好,抽屉从关闭到打开也罢,反正就是界面发生了变化,这些变化统统都是第一步触发功能所引起的副作用。

请特别注意,这里我说的副作用,是一个中性词,而不是贬义词,如果把副作用当作贬义词来理解,则很可能会产生误解。此外,在功能触发之后,App发生了啥,我们是不关心的,所以这里就省去了许多细节,我们关心的仅仅是发生的这些事情所造成的副作用。

因此,我们可以把功能的流程,归纳为下面这样一个高度抽象的流程。


你可以试一下拿这个流程去套你熟悉的任何界面功能,看看是否能覆盖得住。起码在我的场景里,绝大多数的功能是可以覆盖的。

好了,现在我们可以来回答一下“什么是被测功能了”。答案就是,当前App中每一个这样的流程就是一个被测功能,这就是被测功能的定义。每个这样的流程实际上也是一个交互,因此一个交互等价于一个被测功能。

你可以把这个图记一下,它将在后面多次出现。

被测功能的自动发现

前面我们花了一个小节定义了啥是被测功能,那么为啥要花这么大力气来定义被测功能呢?

在回答这个问题之前,请你先想想,通常编写测试用例的过程,包括UT/FT,也包括UI自动化测试用例,主要都是在干啥?要做两个关键的动作:一是构造输入数据,二是对被测代码的输出做校验。明确了编写测试用例的关键动作之后,再回顾一下定义被测功能的那个图,这个图在定义被测功能的同时,还顺便描述了被测功能的生命周期,基于被测功能的生命周期,我们就可以很容易指出编写测试用例的两个关键动作在被测功能中的位置,这对接下来我们自动生成用例代码至关重要。

定义被测功能的另一个重要目的,就是要帮助我们找出这个App具有哪些被测功能

在为传统App手工开发测试用例时,我们似乎不需要或者说从来没想到过要把被测功能给枚举出来。在测试相对规范的团队,被测功能列表往往来自于该软件系统的需求规格说明书,一条需求往往就需要开发多个自动化用例来覆盖,其他测试不是很规范的团队,往往是凭感觉,想到哪测到哪。

今天,我们要为低代码平台打造一个帮助应用做自动化测试的功能,告知应用开发人员当前有多少功能点需要测试,以及它们都是啥,是一个基本需求。此外,可以很容易给出当前App的自动化测试覆盖率统计,这是一个质量指标。最后,被测功能全集是可视化操作的基础,它降低了应用做自动化测试的难度和工作量,可视化操作是低代码平台的典型操作套路。

那么,我们如何检测被测功能呢?

先回顾一下被测功能的定义。

输入数据作为这个流程的第一个环节,我们很容易想到找出所有的输入数据的位置,然后按图索骥,进而找出这个被测功能。但这不是一个好办法,因为输入数据的方式和途径实在是太多了,有的数据输入方式甚至是发生在内存中,比如前面提到过的,抽屉的open属性从false变为true,在浏览器里,要感知内存中某个变量发生了变化,这绝对不是一个容易的事情。

事件机制在几乎所有界面交互模型中都扮演着重要的角色,它能够捕捉和处理用户的交互动作,实现界面的响应和交互功能。无论是Web页面还是其他应用程序,事件机制都是实现交互性的关键机制之一,换句话说,几乎所有功能的触发都离不开事件。也就是说,只要抓住交互事件,就不会遗漏任何被测功能。

跟踪界面的事件是一个非常简单的事情,为啥呢?因为它们是现成的!App开发过程,用户需要添加和配置各种事件来完成App的各种交互功能,因此只要稍微遍历一下App的配置数据,就可以把开发人员配置的事件一个不漏地找出来,并且不会有任何冗余,开发人员不会无端地配置一个事件,任何事件都意味着一个交互的开始。

经过前面的分析,要找出所有的被测功能似乎是唾手可得的,事情真的这么简单吗?显然不是。并不是每个事件都会直接产生副作用。设想一下这个极简单的交互:点击按钮,发起HTTP请求,表格更新。

点击按钮和表格更新没有直接关联,表格的更新是由于HTTP请求的Response事件驱动的。但是HTTP的Response事件不是平白无故出现,它是由于HTTP的请求触发的,而HTTP的请求是由按钮点击触发的。所以,我们可以得到这样一个事件链。


如果发起HTTP请求的逻辑和按钮所在UI离得比较远,则一般不会在按钮点击事件回调中直接发起HTTP请求,而是发起一个自定义事件,由这个事件去驱动一个较远处的逻辑发起HTTP请求,这个过程在引入数据模型层抽象的Web界面上很常见,为了使得业务逻辑与UI保持一定的隔离,会把HTTP请求封装到统一的数据模型中。

在这个情况下,这个事件链就变成了下面这样了。

这里的 query 事件就是一个自定义事件。在此,我们需要对事件进行一下归类,以及讨论一下它们之间的关系。直接给结论,事件可以分为如下四种:

  1. 原生DOM事件,比如click、keyup、load、unload等。
  2. 组件事件,组件内部发出的自定义事件。
  3. 应用的生命周期事件,App的生老病死各阶段触发的事件。
  4. 应用自定义事件,或者简称自定义事件,比如前面事件链里的Query事件。

虽然有这么多种类型,但是根据事件的创建者,我们可以进一步将事件分成两组,第1~3种是一组,它们都不是应用开发人员创建的,应用开发人员只能被动使用它们,但无法创建它们,剩下的第4种是应用开发人员自行创建的,我们将其归为独立的一组。

这样分组后,我们就可以很容易发现,在事件链中,第一组事件往往就是事件链的起点,第二组事件在事件链中是可选的,并且往往处于事件链的中间部位。请记住这个特点,它将帮助我们大幅减少事件链的发现复杂度。

实际上,一个自定义事件很有可能同时驱动2个HTTP请求,此时事件链就变成了事件树了。

当然了,点击一次按钮,完全可以发出多个自定义事件,从而使得事件树变得更加复杂。

上面这个图,是我们的低代码平台Awade根据一个不算复杂的App实际画出来的事件交互树,一个实际商用的App的事件树的复杂度是这个图的几倍甚至十几倍。

事件树的另一个妙用

在上一个小节里,我们跟踪了App的交互事件,最终形成了一棵事件树,这棵树的主要目的固然是用于帮助应用开发人员来测试它们的App,但是这棵树的作用还不止于此,它给出了这个App的交互概览图,以可视化的方式展示了这个App的交互概要信息。这对于开发人员快速了解这个App的交互功能有非常重要的帮助。

在企业级低代码平台的应用中,一个App的生命周期往往是非常长的,像我司这样的企业,一个App的生命周期甚至可以长达数年!所谓铁打的营盘流水的兵,在如此长的生命周期中,App的维护人员是一波又一波地换。新接手App的开发人员,第一个打开的就是这个事件树,从这里快速了解到这个App中各个组件之间的交互情况。了解了App的交互逻辑关系了,这个App的逻辑关系基本就掌握大半了。

我们还为这棵树增加了模糊搜索功能,使得开发人员在实际开发时,可以迅速了解到局部组件交互的细节,从而更快地指导它们完成App的迭代。

小结

现在来回顾一下今天这一讲的内容,我们采用归纳法,从一个典型的界面交互功能开始,归纳出了App的被测功能,并给出了被测功能的定义,就是下面这个图。

被测功能的定义图指出了这些重要信息:

  • 被测功能的生命周期。
  • 在一个被测功能中,并非所有的发生的事情我们都需要关注。
  • 实际上,我们只需要关注功能的触发和产生了啥副作用即可,在这两者之间发生的事情,都可以无视,这大幅简化了接下来我们生成自动化测试用例的复杂度。

基于被测功能的定义,我们抓住了功能触发这个关键环节,并利用它完成了被测功能的自动发现。之所以功能触发这个环节可以被我们利用,其主要原因就是软件界面功能的触发,基本都是通过交互事件的派发和处理来实现的,事件虽然有四种类型,但是这些事件的派发和处理过程基本一样,单一的方式可以大幅简化实现自动发现被测功能的逻辑。

在低代码平台上,应用的任何一行代码都是由低代码平台生成的,低代码平台比开发人员自身还要更加了解这个App,因此,低代码平台能够以一种更加巧妙的方式来帮助应用开发人员实现应用的自动化测试,不仅不需要编写任何测试用例的代码,而且还可以做到自动维护测试用例。当然今天这讲未能全部讲解如何达到这个效果,但是我们开了个好头,接下来的两讲,我会详细说明剩余的内容。

思考题

在讲解自动发现被测功能时,我说了通过事件名就可以找到整个事件链,这中间实际跳过了一个如何分析事件处理逻辑过程,如果现在就让你来补齐这个功能,你将会这么做?这个问题将是我们下一讲要分析和解决的主要内容之一,我希望你现在可以先思考一下。

期待你的分享!我是陈旭,我们下一讲再见。

精选留言(2)
  • 杨春寅 👍(0) 💬(0)

    学习打卡

    2023-10-27

  • ifelse 👍(0) 💬(0)

    只要抓住交互事件,就不会遗漏任何被测功能。 --记下来

    2023-07-29