作为一个工作时长一年半的测试小菜鸟,在自动化测试的探索上有过疑惑、迷茫,也有一些自己的发现和尝试,整理了自己从开始到现在的经历,和大家分享一下

开始接触自动化测试

最早开始做测试的时候是大四实习,接触的是一个智能客服项目,我的工作内容就是在聊天窗口输入对话,系统会根据我的输入判断意图,提取关键词后查询出需要的数据组合成回复返回,而我的工作就是去根据已经有的意图不断设计新的语句,检查返回的对话是否正确,然后把设计出来的语句放到对应意图下,每次发版的时候去每个意图下随机挑选部分语句作为测试数据

这是一个十分枯燥的活,从头到尾都是重复的工作,我在坚持了 3 天之后实在难以忍受这样的方式,开始去尝试解放自己

我的第一个尝试是去用代码调用接口完成测试,而不是通过界面不断输入语句,在搜索了一些资料之后,我制定好了我的计划,使用python+unittest+ddt的以数据驱动的方式进行接口测试,因为整个聊天界面就一个接口,所以脚本很快的编写完成了,没有封装 get/post 等请求,就是最直接的把代码全写在了一个文件,这个测试脚本把我解放了,也是我的自动化测试最开始的第一步

开始编写自己的测试框架

那个我最开始的自动化脚本,以及我之后为了偷懒编写的各种脚本(自动爬取语句,自动组合,自动扫描后台意图配置等脚本)成功给了我转正的机会,我也开始在项目上有更多精力,不断往下深入,从一开始的验证接口功能,到后来我开始负责算法模型的测试,这个过程也了解了一些模型测试的指标,对业务也有更深入的了解,新的挑战开始了

模型需要进行持续优化,但是我们的模型指标却一直没能达到合作方要求(这是一个合作项目),托自动化脚本的福,我基本不需要做太多操作,开发训练好模型后,我执行一下脚本,就是这么一个简单的工作,其余时间我则是负责标注质量的管理,不过我没想到的是,就算是这么一个简单的工作会给我带来了极大的痛苦

因为指标一直不达标,死亡线也越来越近,团队开始陷入了无尽的通宵和加班地狱,我虽然只是需要跑一下测试的脚本,却也不得不和团队一起通宵,直到后面我开始感觉身体撑不住了,我得想办法去解决这个情况,我需要一个开发自己能触发测试的方式

就在这个时候我找到了jenkins,了解到了持续集成,我几乎是毫无犹豫的开始使用它,不过当时因为一些原因,我无法完成整个持续集成流水线,但是我却可以把测试脚本设置为一个动态参数的 job,开发发版后点击构建按钮就可以直接运行测试脚本,获取到测试报告,并且之后推动了运维和开发,我们终于开始使用的 cicd 工具了,还加入了 sonar 和线上的接口定时监控,我终于又解放了自己

不过好景不长,下一个很烦人的场景来了,我们的测试人手不足,但是项目越来越多了,需要测试的接口越来越多,参数越来越复杂,其中一个特殊的计算功能的接口光输入的参数就有 8 种以上,且每种参数又有 10-20 种不同的选择,就算是后面我使用正交表的方式,不断增加的业务和庞大的输入依然让我头疼,我编写不了这么多脚本了

为了从这个不断进行脚本编写的工作中抽身,我开始尝试进行设定接口配置后自动生成测试代码的方式(后面知道了有httprunner的存在,不过因为场景有些特殊,用这个框架对项目不够方便),并为此尝试做了一个自己的测试框架

接口的配置方式

"TestDetail":{
    "name" : "/api/test-detail", # api地址
    "url": "", # 当需要访问和环境变量host不一致的地址时使用这个参数
    "diff": ["b_url"],# 需要进行diff的路由
    "feature": "demo接口", # allure.feature配置
    "method" : "post",
    "head":"{'Content-Type': 'application/json','Authorization':cookie}", # 默认的token获取方式
    "process":{ # 编写不同的调用链
       "TestCase1" : {
        "skip": false, # 是否跳过当前process,比如当前process已被其他接口继承就可以跳过,跳过后不生成代码,也不会触发@pytest.mark.dependency
           "fixture": ["cookie","test_"], # 指定pytest的fixture
           "hooks":  ["transform"], # 请求的参数需要在正式请求前进行特殊处理的话,可以配置需要调用的函数,比如某接口需要从缓存获取验证码,这里可以配一个从缓存读验证码的函数
           "case": [ # 具体的接口参数
               {   
                   "static":{ # 不需要组合的参数,固定的值
                       "page":1
                   }, 
                   "variable": { # 需要进行组合的参数
                       "demo_1":[
                           ["2019/10/08","2019/10/08"],["2019/10/09","2019/10/09"]
                       ],
                       "demo_2":[
                           ["2019/10/08","2019/10/08"],["2019/10/09","2019/10/09"]
                       ],
                   },
                   "comb":"multiply" # 主要是对variable下的参数进行组合,有四种方式,正交,一一对应,笛卡尔积,随机置空
               }
           ],
           "inherit":[ # 继承的上级接口,存在调用链时需要进行配置,通过指定api和process进行前置接口继承的调用,case可以是具体的参数,不指定的时候就按照process的参数组合,data则是需要继承的值
                    {
                        "api": "TestApi",
                        "process": "Case",
                        "case": null,
                        "data": {
                            "demo_3": "TestApi.1.demo"
                        }
                    }
                ],
           "severity":"p0", # allure.severity配置
           "story":"demo测试", # allure.story配置
           "assert":[ # 断言配置,可以调用自定义的函数
               {
                   "value" : "r.status_code == 200",
                   "info" : "接口调用失败"
               },
               {
                   "value" : "assert_check(respone['data']['records'][0])",
                   "info" : "统计结果错误"
               }
           ]
       }
}

自动生成的代码为


@allure.feature(u'demo接口')
class TestTestDetail(object):

   @pytest.fixture(params=CaseData().get_data('demo.json','TestDetail_TestCase1'))
   def testdetail_testcase1_data(self, request):
      return request.param

   @allure.story(u'demo测试')
   @allure.severity('p0')
   @pytest.mark.dependency(name="TestTestDetail::test_testdetail_testcase1")
   @pytest.mark.dependency(depends=["TestTestApi::test_testapi_case"]) 
   def test_testdetail_testcase1(self,cookie,test_,testdetail_testcase1_data):
      #获取TestDetail_TestCase1继承的接口TestApi_Case的值
      #对待测数据进行处理
      r = TestApi(cookie,case).send() 
      respone = r.json()
      testdetail_testcase1_data['TestDetail_TestCase1']['demo_3'] = respone[1]['demo'] 
      #对待测数据进行处理
      testdetail_testcase1_data = transform(testdetail_testcase1_data['TestDetail_TestCase1'])
      #开始进行被测接口的测试
      case = testdetail_testcase1_data
      r = TestDetail(cookie,case).send()
      assert(r.status_code == 200), u"接口调用失败"
      respone = r.json()
      assert(assert_check(respone['data']['records'][0],case['filter_date'],'check_result')), u"统计结果错误"
      #开始进行diff测试
      case = testdetail_testcase1_data
      r1 = TestDetail(cookie,case,b_url).send()
      assert(r1.status_code == 200), u"打卡结果获取失败"
      respone1 = r1.json()
      assert(assert_check(respone1['data']['records'][0],case['filter_date'],'check_result')), u"考勤结果错误"
      result = check(respone,respone1)
      assert(result['code'] == 0),json.dumps(result, sort_keys=True, indent=4)

通过这个工具,自动的组合接口参数,对一些特殊情况,如验证码获取也可以写函数去得到,除了特别特殊的需求外,不需要开发那边为了测试单独配置特殊值或者特定的测试接口。接口被按照断言不同分成了不同的 process,断言也主要是以编写断言规则代码为主(因为测试数据很多,编写断言规则函数是成本最低的),不过因为这个方式需要测试人员进行很多代码编写,甚至需要复写开发逻辑, 所以最终没能在团队内推动成功(团队里的人不想写代码😢

开始探索能被团队接受的自动化方式

后面因为一些个人原因换了工作,来到了一家新公司,团队也比以前大了很多。测试团队拥有拉取代码的权限,并且大家实力都很强(甚至有测试可以在开发没时间时候自己开发产品功能并上线)开始我十分惊喜,觉得在这里应该可以再尝试推动我自己的框架,不过后面发现自己的框架别人用起来并不顺手,而且团队的同事喜欢直接 review 代码,觉得自动化麻烦,没自己看代码快,也不想写太多测试代码,我开始烦恼要怎么去找一个简单快捷的自动化方式

我最终选择的是一个最土的方式,录制 + 回放,在手工测试完成后,再走一次流程,保留下接口的请求和返回,并进行简单的处理,达到不需要进行代码编写直接完成自动化准备的目的,选择的工具是mitmproxy+httprunner,使用 mitmproxy 抓到请求和返回并组合为 httprunner 需要的 yaml 格式,同时对返回的每一个字段都进行断言,那测试最后只需要进行断言的修改和配置 setup,teardown 的 sql 语句就好

自动抓包生成的 api 部分的 yaml 文件为

name: demo
variables:
    account: $account
request:
    url: /demo
    method: POST
    headers:
        Proxy-Connection: "keep-alive"
        Content-Length: "49"
        Pragma: "no-cache"
        Cache-Control: "no-cache"
        Content-Type: "application/json;charset=UTF-8"
        Accept-Encoding: "gzip, deflate"
        Accept-Language: "zh-CN,zh;q=0.9"
    json:
        account: 
            name:$account
validate:
    - eq: ["status_code", 200]

自动抓包生成的 testcase 部分的 yaml 文件为

teststeps:
- api: api/demo.yml
  name: demo
  validate:
    - eq:
      - content.errcode
      - 0
    - eq:
      - content.errmsg
      - ok
  variables:
    account: user

这个工具同时保留下了整个录制的接口调用流程和接口单独的调用数据,还有当接口的请求参数变动时,自动修改 api 的 yaml 文件的功能(因为有时候他们会忘记修改接口文档,所以需要有兼容性)

有了这个工具后,大家相对能接受了一些,也开始尝试在项目上使用了

继续前进

目前还有很多想法,比如开发开始使用 swagger 了,想配合 yapi 把这个目前的测试工具做成服务端,可以一键把当前配置好的的 yaml 打到 yapi 的数据库(不直接使用 yapi 是因为大家更喜欢用 ide 的方式写测例),这样给开发一个在 cicd 前能调用接口测例自测的方式,之前兼容参数变动的功能也可以改成检测前端请求参数异常并提示

还有团队伙伴需要帮忙写一个自动插入测试数据的工具,目前和他约定好了格式

testdata:
- name: 创建a库和b库数据的依赖数据
  num: 100
  data:
      static:
          a.user.adress: "test"
      variable:
          a.user.id: 由4自增
          b.user.name: 随机人名
      bind:
        - a.user.id -> b.user.id

- name: 创建c表数据
  databese: c
  data:
      variable:
          user.id: (int) 1-10
          user.puid: (float) 1.2-1.4
          user.sex: [['1'],['2','3']]
          user.case: ["test","test1","test2"]
          user.data: (date) 2020/01-2001/12

之后可以一键完成测试数据准备,清理并且还可以尝试和当前的工具结合

最近又从阿里妈妈开源的 MagicOTP 得到了启发,想尝试推动 goreplay+ 规则配置进行线上流量测试的方式,也在推动团队开始使用 elk(运维早就部署了,但是完全没人去用😓

从一开始对测试的肤浅理解,到现在不断的尝试用不同方式去提高项目质量,自动化测试的道路充满了挑战但是又让我觉得十分有趣,从一开始完全不知道自动化到现在不断尝试去推动自动化,改善自动化,这些经历给我最大的启示是,一切行为只是手段,保证质量,赋能团队才是目的,从开始进行自动化之后,对项目质量的保证有了更多手段,也有更多时间去进行探索,我十分享受这个过程,也觉得这是其他职位给不了对特殊感受,一路上走来也是踩完一个坑刚出来就进下一个坑,之后还有很多挑战,我的测试之路才刚刚开始,不忘初心,共勉


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