# 21|设计实战(一): 一个金融交易业务的自动化测试设计 你好,我是柳胜。 一提到金融业,可能你第一时间想到的就是钱。没错,存款、转账都是普通人最常接触的金融业务。不过在专业金融人士那里,钱的形态和流动方式也更加复杂,有交易、投资、期货、股票、基金等等金融模型。 从测试角度来看,金融业软件有两个特点:第一,对数据精准性要求非常高,不能出错,bug的成本极大;第二,软件的设计要求有很强的金融领域知识,也很复杂。 所以,金融行业对软件的交付质量要求高,对软件测试非常重视,自动化测试的投入也很大。而自动化测试设计的过程,其实也是对复杂业务梳理的过程。 挑战随之而来,用我们的Job模型,能不能让金融系统的复杂业务变得简单?今天我们就一起结合例子来看一看。 ## 一个转账案例 我们来看后面这个转账案例。 ![图片](https://static001.geekbang.org/resource/image/14/b0/146f4098a2411060fee7aee3c0423cb0.jpg?wh=1920x636) 这是一个类型为正例的测试案例。金融业测试里,要设计很多的测试案例,正例是那些预期能够正常完成业务的测试案例,而反例是由于违背了业务规则,预期无法成功完成业务的测试案例。 在我们的测试案例里,交易账户都是正常的状态,交易额度也在安全额度之内,预期结果是能够正常完成转账交易,所以类型是正例。 ## Job数据建模 了解了案例情况,我们想用Job模型来为它建模,该怎么做呢? 金融业务里最关键的要素就是数据,我们可以从数据角度来理一下测试案例。 这里的数据有账户数据,包括转出账户**A**和转入账户**B**;还有交易数据,包括转账类型**同行转账**和转账金额**4000**;另外,还有配置数据,单笔转账限额**5000**和交易超时时间**30分钟**等。 ![图片](https://static001.geekbang.org/resource/image/e1/0d/e14960ee123ed1cf08bfbfa5111cc00d.jpg?wh=1920x855) 我们Job模型有三个传递数据的地方,分别是JobInput、TestData和TestConfig。 Input是从Job外部传递进来的数据,TestData是Job自带的测试数据源,能够多组数据循环运行Job,TestConfig是Job会引用到的比较固定的配置数据。 想一想,账户数据和交易数据,需要放在JobInput里,还是TestData里呢?这个要看我们自动化测试整体设计。 如果我们每次测试运行都要创建新的测试账户,那转账Job就可以从Input里获得其他Job创建好的账户信息;反之,测试账户是存量的数据,那我们应该把它放在TestData里,实现“多份数据,一份代码”,这样才能提高转账Job的ROI。 而单笔转账限额和交易超时时间,我们把它放在TestConfig里。表格更新如下: ![图片](https://static001.geekbang.org/resource/image/5e/10/5eb82f438393149147da953799f88610.jpg?wh=1920x909) Job模型建模如下: ![图片](https://static001.geekbang.org/resource/image/9c/42/9ca49d1c9503692040a5052ed0562842.jpg?wh=1920x682) ## Job行为分解 数据研究完了,我们再来看梳理一下测试案例的操作步骤。 1.登录系统 2.发起转账 3.验证转账 4.退出系统 登录系统和退出系统这两个行为是通用的,为了提高Job的ROI,我们可以把登录系统和退出系统独立出来2个Job。而发起转账和验证这2个步骤,是作为1个Job,还是分割成2个Job呢。这取决于转账和验证的步骤复杂度,在刚开始时,我们不要过度工作over work,可以把转账+验证先作为一个Job。 这样的话,转账Job就分解成了3个子Job:登录,转账和验证,退出。在Web上实现的话,我们选择用Selenium作为开发工具。 ![图片](https://static001.geekbang.org/resource/image/6a/15/6a2ab0312d834c699b66fa974e29aa15.jpg?wh=1920x1069) 因为子Job继承了父Job的属性,现在3个子Job应该可以从TestData里拿到账户信息,**登录Job**用A账户进行登录,**转账验证Job**使用A账户给B账户转账4000,**退出Job**退出A账户。 现在自动化测试开发人员就可以用Selenium工具进行开发了! ## Job扩展 自动化测试开发人员开发出这3个Job后,就完成了转账自动化测试第一个版本。能实现跑起来的基础目标。 但要想让它稳定运行,我们还需要理清楚金融业务的关联性。比如,银行账户是有复杂的配置的,光账户状态就有几十种,比如时效、锁定、冻结、挂失等等。想要让转账测试运行成功,测试账户必须在正常的状态下,有转账的权限,而且还要有足够的余额等等这些条件。 这些都是转账业务成功运行的前提条件,如果不满足这些条件,直接开始运行转账Job,可能会遇到各种“错误”。但这些“错误”是正常的预期,所以,诊断它们只会浪费你的时间。 ### 数据验证 怎么办?为了保证转账Job的稳定运行,我们可以增加一个**ValidateJob**来验证数据。让转账Job的依赖指向ValidateJob,如果ValidateJob运行通过,转账Job才会运行;否则,转账Job就不必运行。 ValidateJob负责做数据验证,我们可以用[Great Expectation](https://greatexpectations.io) 来完成这个验证。 Greate Expecation是一个Python的数据验证框架,它支持多种数据源,内嵌规则引擎。为了让你直观了解它的使用过程,我为你准备了后面的流程图。 ![图片](https://static001.geekbang.org/resource/image/e0/05/e052642764dba3c76a21182756b2dc05.jpg?wh=1920x961) 我们的账户存在MySQL数据库里,可以在greate\_expectation.yaml里配置数据库信息: ```yaml datasource_yaml = r""" name: my_mssql_datasource class_name: Datasource execution_engine:   class_name: SqlAlchemyExecutionEngine   connection_string: mssql+pyodbc://sheng.liu:Welcome1@:/?driver=&charset=utf&autocommit=true #要验证数据集是Account表 table: account ``` 然后,就可以在Jupyter Notebook里开发数据验证代码了: ```python #验证账户信息 batch.load(greate_expectation.yaml) #验证规则1:账户的State字段不为空,而且是Active状态 batch.expect_column_values_to_be_equal('State','Active') #验证规则2:账户余额必须大于4000 batch.expect_column_values_greater_than("balance",4000) ``` 运行代码,Great Expectation会从数据库里读取账户数据,进行数据验证,2条规则都通过,整个Job运行成功,会产生下面的报告。 ![图片](https://static001.geekbang.org/resource/image/55/85/553b5ea8ce8c11026b95803d83040a85.jpg?wh=1920x695) 现在,我们把Validate数据Job添加到Job设计图里,更新如下。 ![图片](https://static001.geekbang.org/resource/image/40/f3/40739be914fb12409f3be5c89817f7f3.jpg?wh=1920x1069) 这样,每次转账Job运行之前,都会先运行Validate数据Job。只有数据都准备好了,转账Job才会运行。在这个机制的保护下,转账Job的运行结果成功或失败,就更可信了。 ### 反例测试 有了ValidateJob之后,我们可以考虑反例测试了。这里反例的逻辑是这样的:如果账户数据是异常的情况下,转账Job是会失败的,要是交易依然成功,那就是一个安全漏洞。 因为这种漏洞在金融业务里很可能带来巨大的经济损失,所以,在金融测试里,反例测试也是很重要的测试案例。 不同的反例条件,转账执行失败的结果也是不一样的,它们有一个对应关系。 我列了一个条件-结果表格,这样你能一目了然。 ![图片](https://static001.geekbang.org/resource/image/0a/23/0a26db5a405ac2ed7083838849fa5923.jpg?wh=1920x776) 那自动化设计里,反例测试该怎么做? 你可能在第一时间想到的办法是,开发一个“转账Job-反例”,在这个反例Job里,把上面表格里的条件和结果,作为测试案例里的Given条件和Then预期输出。 这就引出了下一个问题,这个反例Job的数据从哪里来?其实,这种情况下,我们就可以利用Job模型的Input、Output机制,让ValidateJob验证数据之后,把数据通过Output分发到转账正例和转账反例的Job中去。 Greate expectation有一个Action的机制,根据规则匹配的结果,即可触发不同的Action,配置文件如下: ```python validation_operators:     action_list_operator:         class_name: ActionListValidationOperator         action_list:         - name: StateNotSatisfied # 状态不符合           action:             class_name: Output             output: "State not satisified"         - name: BalanceNotSatisfied # 余额不符合 action: class_name: Output output: "Balance not satisified"  ``` 现在我们再次重构Job设计树,就会得到下面这个设计结构。我们在跟转账正例Job平行的层级,再加上一个转账反例Job。不难看出,正例和反例的数据都来自于一个ValidateJob的输出。 ![图片](https://static001.geekbang.org/resource/image/f3/02/f3709f1c2367eb03690c443305a36a02.jpg?wh=1920x1156) ValidateJob专门负责账户的合法性验证,使用Greate Expectation工具,内嵌规则引擎,来加载和解析账户数据,并把账户数据分组通过Output输出。然后,转账的正例Job拿到合法的数据,反例Job拿到非法的数据。 我给你打个比方,帮助你理解一下这个工作原理。其实这个场景就像生产线,第一个工人只管生产螺丝钉,他很专业,可以生产不同型号的螺丝钉,他不需要知道谁会使用这些螺丝钉。他需要做的就是,做好螺丝钉后,就把他们放到传送带上;而第二个工人是造飞机的,他会从传送带上拿到飞机螺丝钉,然后开始自己的任务;第三个工人是造汽车的,他拿到汽车的螺丝钉也开启自己的任务了。 这样分工,每个工人都在干自己的专业工作,又通过传送带和其它工人完成了协作。放在软件设计上,就满足了高内聚,低耦合的设计原则。 好,咱们接着推演后续可能会怎样发展。如果哪一天,ValidateJob里面越来越复杂了,一个人无法完成,需要分解给10个人去做。那么只需要保持ValidateJob接口不变,在ValidateJob下面分解成10个子Job就可以。 就像生产线上,生产螺丝钉的人手不够用了,他是招8个人还是10个人,分成几个小组,这些都是这个小组内部的事,只要这一组,还是跟以前一样,对外提供不同型号的螺丝钉就可以了。 好,让我们回顾一下整个过程。我们从一个银行转账正例案例开始,向下细化成3个Selenium Job,又平行引入了数据验证Job和反例Job。 到这里,你是不是对怎么应用Job模型更熟练了呢?我们利用这个模型,根据业务需要来扩展Job树。**具体扩展方向有两种,一种是向下去下钻,将测试场景分解细化;另一种是向上聚合抽象,把场景归类合并到一起,形成一个更大的场景。** 最后,我们设计好了正例和反例Job,就能聚合一起,生成一个转账的正反测试Job。 ![图片](https://static001.geekbang.org/resource/image/56/5e/56a32700173fbf2e6e5f991ab75f655e.jpg?wh=1920x1634) 其实这个Job树并不是“终点”,我们还可以根据业务的复杂度再进行细化。 金融系统里,数据验证是一个精细的工作,确保无误。在手机端、Web端、API,甚至DB都要验证数据。根据这个需求,我们可以把转账+验证Job,进一步分解,分解后的情况如下。 ![图片](https://static001.geekbang.org/resource/image/c4/30/c49d0d3f10f03072cf40fc0085832a30.jpg?wh=1920x1483) 你可能还有疑惑,不知道要怎么判断,我们是否得到了一个足够细化的最终结果。其实判断起来也不难,终点就是业务逻辑梳理清楚后,这棵Job树的所有叶子结点,都明确了实现工具。这样,我们就可以推进分工,进入开发实现阶段了。 我在反例Job里,没有标注蓝色的实现工具,你可以考虑一下怎么细化。 ## 小结 在今天的例子里,我们通过Job树的设计,梳理了金融业务的数据流动和验证。而且由于金融业务的特殊性,尤其是对数据精准性的要求。我还带你认识了一款数据验证框架**Greate Expecation**。它是基于Python语言的框架,能够从数据源里获得数据,进行规则验证,输出结果,而且在Jupyter Notebook里开发,交互性和效率都很好。 这整个的建模过程,经历了从整体到局部,从概要到细节的推演。其实,直到这棵Job树的叶子结点都有了选型好的工具、可开发的Job,我们的交易案例设计才算清楚了,这也是业务梳理完毕的信号。 每一个叶子结点的Job就是一个开发任务,有接口、有数据、有配置,开发工作任务列表就出来了。而且,通过Job之间的依赖关系,我们也可以得出一个有优先顺序的开发执行计划,谁在前,谁在后。 今天这一讲,为了让表达更形象,我们沿用了树形图来推演设计。在实际工作中,我们可以把这个设计保存在YAML或XML文件里,方便去做验证和二次开发,我会在设计篇最后一讲说说这块的实现思路。 下一讲,我们会继续锻炼自动化设计思维,探索如何利用Job模型实现自动化部署管线的设计,敬请期待。 ## 思考题 你的项目里有没有涉及数据验证测试,你是怎么做的? 欢迎你在留言区和我交流互动,也推荐你把今天学的内容分享给更多同事、朋友。