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,与实际比较

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

猜你喜欢

转载自www.cnblogs.com/fancyl/p/9172569.html
今日推荐