mali | 32 min (8603 words)

自动化接口用例从 1 到 1000 过程中的实践和思考

引言

当一个新人刚加入公司的时候,我们通常告诉新人怎么去写一个自动化用例:从工程配置到如何添加接口、如何使用断言,最后到如何将一个用例运行起来。
而在实际工作和业务场景中,我们常常面临着需要编写和组织一堆用例的情况:我们需要编写一个业务下的一系列的自动化接口用例,再把用例放到持续集成中不断运行。面临的问题比单纯让一个用例运行起来复杂的多。
本人加入有赞不到一年,从写下第 1 个 case 开始,持续编写和运行了 1000 多个 case ,在这过程中有了一些思考。在本文中,和大家探论下如何编写大量自动化接口用例以及保持结果稳定。

一、执行效率

目前使用的测试框架是基于 spring ,被测接口是 dubbo 的服务。 dubbo 的架构如图(源自官网)服务使用方的初始化需要经历以下这几个步骤: 
1. 监听注册中心 
2. 连接服务提供端 
3. 创建消费端服务代理
本地调试用例时,发现速度非常慢,运行一个用例需要 30s,而实际执行用例逻辑的时间大概在 1s 左右,主要时耗在服务消费者的初始化阶段。
测试工程中,各服务的 test 类继承了同一个基类,基类里面做了各服务的初始化的步骤。在对接的服务数目较少时,需要初始化的对象较少,对用例运行的影响并不大,但随着业务的增多,服务数目也增多,导致跑 A 服务接口的用例时把大量未用到的 B 服务、C 服务也一起初始化了,导致整体时耗大大增加。
解决办法:在运行用例时只初始化需要的服务使用方,减少不必要的初始化开销。

二、用例编写和维护

一个用例示例

以一个简单的业务场景为例:商家可以在后台创建会员卡给店铺的会员领取,商家可以对会员卡进行更新操作,这里需要有一个自动化用例去覆盖这个场景。用例编写的基本步骤为:

转换成代码为:

@Test
    public void testUpdate() {
        try {
            /*
             * 创建新建和更新的卡对象
             */
            CardCreateDescriptionDTO descCreate = new CardCreateDescriptionDTO();
            descCreate.setName(xxxx);
            //此处省略若干参数设置过程....
            CardUpdateDescriptionDTO descUpdate = new CardUpdateDescriptionDTO();
            descUpdate.setName(xxxxx);
            //此处省略若干参数设置过程....
            /*
             * 新建会员卡
             */
            cardAlias = cardService.create((int) kdtId, descCreate,operator).getCardAlias();
             /*
             * 更新会员卡
             */
            cardService.update(kdtId, cardAlias, descUpdate, operator);
            /*
             * 校验编辑是否生效
             */
            CardDTO cardDTO = cardService.getByCardAlias(cardAlias);
            Assert.assertEquals(cardDTO.getName(), xxxx, "会员卡更新失败");
            //此处省略若干参数校验过程....
        } catch (Exception e) {
            Assert.assertNull(e);
        } finally {
            try {
                if(cardAlias!=null) {
                    cardService.deleteByCardAlias((int) kdtId, cardAlias, operator);
                }
            } catch (Exception e) {
                Assert.assertNull(e, e.getMessage());
            }
        }
    }
123456789101112131415161718192021222324252627282930313233343536373839

按照预期的步骤去写这个 case ,可以满足要求,但是如果需要扩展一下,编写诸如:更新某种类型的会员卡、只更新会员卡的有效期这样用例的时候,就会觉得按这个模式写 case 实在太长太啰嗦了,痛点在以下几个地方:

用例本身关注的是更新这个操作,却花了太多时间和精力在其他地方,很多是重复劳动。代码编写里有一个重要原则,DRY(Don't Repeat Yourself),即所有重复的地方都可以考虑抽象提炼出来。

三段式用例

可以将大部分用例的执行过程简化为三个部分: 
1. 数据准备 
2. 执行操作 
3. 结果检查
用简单的三个部分来完成上述用例的改写:
数据准备

@DataProvider(name="dataTestUpdate")
    public Object[][] dataTestUpdate() {
        return new Object[][]{    
{cardFactory.genRuleNoCreate(...),cardFactory.genRuleNoUpdate(...)},
{cardFactory.genRuleCreate(...),cardFactory.genRuleUpdate(...)},
{cardFactory.genPayCreate(...),cardFactory.genPayUpdate(...)}
       };
    }
123456789

执行操作 + 结果检查

Test(dataProvider = "dataTestUpdate")  
   public void testUpdate(CardCreateDescriptionDTO desc,CardUpdateDescriptionDTO updateDesc){                        
       try {    
           /*
           * 执行操作:创建+更新
           */                            
           //创建会员卡                                                     
           CardDTO cardBaseDTO = createCard(kdtId,desc,operatorDTO);        
           cardAlias=cardBaseDTO.getCardAlias();                                          
           recycleCardAlias.add(cardAlias); //将卡的标识放入垃圾桶后续进行回收                                                    
           CardDTO ori = getCard(kdtId,cardAlias);     
          //更新会员卡                            
          updateCard(kdtId,cardAlias,updateDesc,operatorDTO);                                                                                            
           CardDTO updated = getCard(kdtId,cardAlias);    
           /*
           * 结果检查
           */                          
      checkUpdateCardResult(ori,updated,updateDesc,kdtId);                                               
       } catch (Exception e) {                                                                   
           Assert.assertNull(e);                                                                 
       }                                                                                         
12345678910111213141516171819202122

其中可行的优化点将在下面娓娓道来。

测试数据的优化

在这个用例中,数据准备的部分使用了 dataProvider 来复用执行过程,这样不同参数但同一过程的数据可以放在一个 case 里进行执行和维护。
数据生成使用了工厂方法 CardFactory ,好处是简化了参数,避免了大量 set 操作(本身包装的就是 set 方法);另一方面,根据实际的业务场景,可以考虑提供多个粒度的构造方法,比如以下两个构造方法需要提供的参数差别很大:

最后,用例执行完成后需要清理资源。这里的清理资源采用的是一个全局的 list 的方式保存需要清理的资源信息,在用例执行过程中往里增加数据:(recycleCardAlias.add(cardBaseDTO.getCardAlias());), 然后用对应的方法取其中的数据进行删除,类似垃圾桶。与原有执行完就执行清理动作相比,使用垃圾桶更加灵活,可以选择控制下清理频率。
比如每次在 AfterMethod 或 AfterClass 中去清理。

//统一回收
    @AfterMethod
    public void tearDownMethod() {
        for(int i =0;i<recycleCardAlias.size();++i) {
            try {
                deleteCard(kdtId, recycleCardAlias.get(i), cardOperatorDTO);
            } catch (Exception e) {
                logger.error("clear card fail: " + recycleCardAlias.get(i));
            }
        }
        recycleCardAlias.clear();
    }
12345678910111213

对方法的适度封装

在实际编写用例的时候,有两个地方可以考虑进行方法封装,从来简化调用,方便维护:


↙↙↙阅读原文可查看相关链接,并与作者交流