一.数据驱动/代码驱动优缺点:
使用数据驱动的好处:
- 代码复用率高。同一测试逻辑编写一次,可以被多条测试数据复用,提高了测试代码的复用率,同时可以提高测试脚本的编写效率。
- 异常排查效率高。测试框架依据测试数据,每条数据生成一条测试用例,用例执行过程相互隔离,在其中一条失败的情况下,不会影响其他的测试用例。
代码驱动:
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,与实际比较
如果数据需要加密或者其他操作,可以再封装方法去实现,该框架就举例到此。