• unittest框架扩展(基于代码驱动)自动化-下


    一.数据驱动/代码驱动优缺点:

    使用数据驱动的好处:
    - 代码复用率高。同一测试逻辑编写一次,可以被多条测试数据复用,提高了测试代码的复用率,同时可以提高测试脚本的编写效率。
    - 异常排查效率高。测试框架依据测试数据,每条数据生成一条测试用例,用例执行过程相互隔离,在其中一条失败的情况下,不会影响其他的测试用例。
    代码驱动:
      1.测试用例全是用代码实现的。
      2.接口之间互相有依赖的,需要操作数据库、参数加密、操作redis的。

    比如,流程:先注册-->登录-->加入购物车-->下单-->付款,整个流程每一个步骤的数据都要基于前一个接口的调用,因此利用数据驱动是不能满足需求的,需要用代码驱动来实现。
    数据驱动
      1.适合 有大量的接口需要测试,接口之间互相不依赖的

    二.哪些业务需要做自动化:

      1、重要的接口
      2、主要的流程
      3、优先验证正常的

    三.本文将继续完善上一篇https://www.cnblogs.com/fancyl/p/9167015.html框架,基于扩展该框架既可以支持数据驱动,也可以支持代码驱动的一个框架。

    实现:

    1.先注册,注册完登录,登陆后抽奖

    业务逻辑:

      ①.注册接口要返回账号,密码给登录,登录后返回userid,sign给抽奖接口;

      ②.注册、登录的信息存在数据库,因此要有操作数据库流程;

      ③.因为抽奖次数存放在Redis里,每抽奖3次需因此要有操作redis的流程;

    2.以前有做过操作redis的封装接口,如下:

    import redis
    class MyRedis():
        def __init__(self,ip,passwd,port=6379,db=0):  #构造函数
            try:
                self.r = redis.Redis(host=ip,password=passwd,port=port,db=db)
            except Exception as e:
                print('redis连接失败,错误信息%s' %e)
        def str_get(self,k):  #获取数据要有返回值,所以要有返回值
            res = self.r.get(k)
            if res:
                return res.decode()
            return None  #写不写都行
        def str_set(self,k,v,time=None):
            self.r.set(k,v,time)
        def delete(self,k):
            tag = self.r.exists(k)  #判断这个key是否存在
            if tag:
                self.r.delete(k)
                print('删除成功')
            else:
                print('这个key不存在')
        def hash_get(self,name,k):  #无论key是否存在,都不会报错,所以不用写try
            res = self.r.hget(name,k)
            if res:
                res.decode()
        def hash_set(self,name,k,v):
            self.r.hset(name,k,v)
        def hash_getall(self,name):
            data = {}
            res = self.r.hgetall(name)
            if res:
                for k,v in res.items:
                    k = k.decode()
                    v = v.decode()
                    data[k]=v
            return data
        def hash_del(self,name,k):
            res = self.r.hdel(name,k)
            if res: #因为删除成功会返回1,删除失败返回0
                print('删除成功')
                return 1
            else:
                print('删除失败,该key不存在')
                return 0
        @property    #定义为属性方法,以后可以直接调用
        def clean_redis(self):
            self.r.flushdb()  #清空redis
            print('清空redis成功!')
    
    my = MyRedis(**REDIS)  #**REDIS,把配置文件中的my_redis里的参数变成xx=xx。在这儿实例化以后,在其他页面用的之后直接导入就可以使用了

    3.封装MySQL的接口:

    import pymysql
    from conf import setting
    class MyDb(object):
        def __del__(self):
            #析构函数
            self.cur.close()
            self.coon.close()
        def __init__(self,
                     host,user,passwd,db,
                     port=3306,charset='utf8'):
            try:
                self.coon = pymysql.connect(
                    host=host,user=user,passwd=passwd,port=port,charset=charset,db=db,
                    autocommit=True,#自动提交
                )
            except Exception as e:
                print('数据库连接失败!%s'%e)
            else:
                self.cur = self.coon.cursor(cursor=pymysql.cursors.DictCursor)
        def ex_sql(self,sql):
            try:
                self.cur.execute(sql)
            except Exception as e:
                print('sql语句有问题,%s'%sql)
            else:
                self.res = self.cur.fetchall()
                return self.res
    
    
    
    my_sql = MyDb(**setting.MYSQL_INFO)
    #直接在这里实例化的话,用的时候,直接导入就ok了

    4.因为在接口调用过程中会多次用到发送请求的接口:get或post,再此将发送请求的方法封装后,可随时调用:

    import requests
    from conf.setting import log  #日志模块,在setting中有实例化,因此可以直接导入使用
    
    class MyRequest():
        @staticmethod #因为不想实例化,所以用静态方法
        def post(url,data=None,cookie=None,header=None,is_json=False,files=None):
            data = data if data else {}  #三元表达式,判断传值是否为空,为空就定义为空字典
            #拆分:
            # if data:
            #     data = data
            # else:
            #     data = {}
            cookie = cookie if cookie else {}
            header = header if header else {}
            files = files if files else {}
            try:
                if is_json: #如果是Json
                    res = requests.post(url,json=data,cookies= cookie,headers = header,verify=False,files=files).json()
                else:
                    res = requests.post(url, data=data, cookies=cookie, headers=header,verify=False,files=files).json()
                log.debug('【接口返回数据:%s】'% res) #打印日志,实际返回的结果
                print('res...',res)
            except Exception as e:
                res = {'error':str(e)}  #如果接口调用出错的话,那么就返回一个有错误信息的字典
                log.error('异常信息:接口调用失败! url 【%s】 data 【%s】 实际结果是 【%s】'%(url,data,res))
            return res
    
    
    #函数的默认参数。不要写成字典或者list。这样会有问题
    
        @staticmethod
        def get(url,data=None,cookie=None,header=None):
            data = data if data else {}
            cookie = cookie if cookie else {}
            header = header if header else {}
            try:
                # verify=False 的意思就是https能访问,如果不加这个参数,https访问时会报错
                res = requests.get(url, params=data, cookies=cookie, headers=header,verify=False).json()
                log.debug('【接口返回数据:】'%res)
            except Exception as e:
                log.error('异常信息:接口调用失败! url 【%s】 data 【%s】'%(url,data))
                res = {'error':str(e)}  #如果接口调用出错的话,那么就返回一个有错误信息的字典
            return res

    5.其中setting.py,case_data文件,tool.py等文件是没有变化的,需在cases下新增代码用例,比如先新建Choice.py文件做抽奖接口,抽奖的业务:首先用户注册-->登录-->抽奖,而登录需要注册返回的username,passwd,抽奖需要登录返回的user_id,sign,具体实现如下:

    import unittest,requests
    from lib.my_redis import my
    from lib.my_sql import my_sql
    from conf.setting import BASE_URL
    from urllib.parse import urljoin
    from lib.my_request import MyRequest
    
    
    class TestChoiceDraw(unittest.TestCase):
        def setUp(self):  #每个用例执行之前执行的
            self.username = 'test_lyl'  #类变量,所有函数都可以使用
    
        def tearDown(self):#每个用例执行之后,执行该函数
            sql = 'delete from app_myuser where username = "%s";'% self.username  #每个用例执行后执行的,将注册用户删掉,不然下次注册会报错
            my_sql.ex_sql(sql)  #调用my_sql函数,执行sql语句
            my.delete('choujiang:%s'%self.username)  #每个用例执行结束后,执行删除redis里面的数据
    
        def register(self):  #注册函数,在返回username,passwd供其他函数调用
            url  = '/api/user/user_reg'
            real_url = urljoin(BASE_URL,url)  #拼接实际Url
            username = self.username
            passwd = 'xxxxxxx'
            data = {'username':username,'pwd':passwd,'cpwd':passwd}
            res = MyRequest.post(real_url,data)  #res实际调用myrequest里的post请求结果,传参url,data
            self.assertEqual(1000,res.get('error_code'),msg='注册失败')  #1000是预期结果,实际结果对比,不一致返回msg
            return username,passwd
    
        def login(self): #登录函数,返回user_id,sign,供抽奖时使用
            url = '/api/user/login'
            real_url = urljoin(BASE_URL, url)
            username,passwd = self.register()  #获取注册函数里的username和passwd
            data = {'username': username, 'passwd': passwd}
            res = MyRequest.post(real_url,data) #res实际调用请求结果
            self.assertEqual(0, res.get('error_code'), msg='登录失败') #预期结果:登录成功返回0,与实际返回结果比较
            user_id = res.get('login_info').get('userId') #获取userId
            sign = res.get('login_info').get('sign') #获取sign
            return user_id,sign
    
        def test_choice(self): #函数以test开头会被首先执行该用例,然后该用例调用login函数,login函数调用register函数,而login和register函数没有以test开头,否则会被执行两次
            '''正常抽奖'''    #正常抽奖指的是抽奖次数不超过3次
            url = '/api/product/choice'
            real_url = urljoin(BASE_URL, url)
            user_id,sign = self.login() #获取login()里的user_id,sign
            data = {'userid':user_id,'sign':sign}
            res = MyRequest.get(real_url,data)
            self.assertEqual(0, res.get('error_code'), msg='抽奖接口调用失败') #抽奖成功返回0
    
        def test_choice_fail(self):
            '''测试超过抽奖次数的'''
            url = '/api/product/choice'
            real_url = urljoin(BASE_URL, url)
            user_id,sign = self.login() #获取login()里的user_id,sign
            data = {'userid':user_id,'sign':sign}
            # choujiang:username
            key = 'choujiang:%s'%self.username #这个redis里面的key,控制抽奖次数的
            my.str_set(key,3,180)  #设置抽奖次数3次,180s失效
            res = MyRequest.get(real_url, data) #调用抽奖接口
            self.assertEqual(1099,res.get('error_code'))  #抽奖次数用尽的时候返回1099,因此作比较

    6.添加商品接口,具体业务逻辑:先注册,注册完之后把注册用户改为管理员,然后登录获取到session信息,然后添加商品,添加后再清理用户信息和商品信息。否则无法再次添加。具体实现如下:

    import os
    import unittest,requests
    from lib.my_redis import my
    from lib.my_sql import my_sql
    from conf.setting import BASE_URL,DATA_PATH
    from urllib.parse import urljoin
    from lib.my_request import MyRequest
    class Product(unittest.TestCase):
        def setUp(self):
            self.username = 'test_lyl'
            self.product_name = 'lyl_测试商品'
            #在这里定义的话,每个函数都可以用了
        def tearDown(self):
            sql = 'delete from app_myuser where username = "%s";'% self.username  #将注册用户删除
            my_sql.ex_sql(sql)
            sql2 = 'delete from app_product where product_name = "%s";'%self.product_name  #将注册成功后添加的商品删掉,否则无法二次添加
            my_sql.ex_sql(sql2)
            print('数据清理完成。。。')
        def register(self):
            url  = '/api/user/user_reg'
            real_url = urljoin(BASE_URL,url)
            username = self.username
            passwd = 'xxxxxx'
            data = {'username':username,'pwd':passwd,'cpwd':passwd}
            res = MyRequest.post(real_url,data)
            self.assertEqual(1000,res.get('error_code'),msg='注册失败')
            sql = 'update app_myuser set is_admin = 1 where username = "%s";'%username
            my_sql.ex_sql(sql)
            return username,passwd
    
        def login(self):
            url = '/api/user/login'
            real_url = urljoin(BASE_URL, url)
            username,passwd = self.register()
            data = {'username': username, 'passwd': passwd}
            res = MyRequest.post(real_url,data)
            self.assertEqual(0, res.get('error_code'), msg='登录失败')
            user_id = res.get('login_info').get('userId')
            sign = res.get('login_info').get('sign')
            return user_id,sign
    
        def test_add_product(self):
            '''测试添加奖品信息'''
            url  = '/api/product/add'
            real_url = urljoin(BASE_URL,url)
            userid,sign = self.login()  #获取登录返回的session
            product_name = self.product_name  #在setUp()定义,否则在删除商品时找不到product_name
            file_abs_path = os.path.join(DATA_PATH,'test.jpg') #存放图片或文件的路径
            #把文件的绝对路径拼好
            files = {'file':open(file_abs_path, 'rb')}  #这个构造好文件信息
            data =  {'userid':userid,'sign':sign,'name':product_name} #这个是请求数据
            res = MyRequest.post(real_url,data=data,files=files) #获取请求结果
            self.assertEqual(1000,res.get('error_code'),msg='添加奖品失败') #添加成功返回1000,与实际比较

    如果数据需要加密或者其他操作,可以再封装方法去实现,该框架就举例到此。

  • 相关阅读:
    leetcode——832. 翻转图像
    leetcode——830. 较大分组的位置
    leetcode——1089.复写零
    leetcode——86. 分隔链表
    leetcode——387. 字符串中的第一个唯一字符
    leetcode——389. 找不同
    leetcode——61. 旋转链表
    leetcode——24. 两两交换链表中的节点
    leetcode——817. 链表组件
    leetcode——234. 回文链表
  • 原文地址:https://www.cnblogs.com/fancyl/p/9172569.html
Copyright © 2020-2023  润新知