• 七、Python-Unittest、文件解析、钉钉发送消息、自动化测试案例


    (一)Unittest前置条件

    1、所有用例运行之前,它会执行一次

    @classmethod

    def setUpClass(cls):

    2、所有用例运行完之后,它会执行一次

    @classmethod

    def tearDownClass(cls):

    3、每条测试用例运行之前都会先执行它

    def setUp(self)

    4、每条测试用例运行之后都会执行它

    def tearDown(self)

    5、实例如下:

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    # @FileName  :用例前置条件.py
    # @Time      :20201028 0028 22:22
    # @Author    :Krystal
    # @Desc      :Testcase
    
    import unittest
    
    class Test(unittest.TestCase):
        @classmethod
        def setUpClass(cls):
            # 所有用例运行之前,它会执行一次
            print('SetUpClass')
    
        @classmethod
        def tearDownClass(cls):
            # 所有用例运行完之后,它会执行一次
            print('tearDownClass')
    
        def tearDown(self):
            # 每条测试用例运行之后都会执行它
            print('tearDown')
    
        def setUp(self):
            # 每条测试用例运行之前都会先执行它
            print('setUp')
    
        def testa(self):
            print('testa')
    
        def testz(self):
            print('testz')
    
        def testb(self):
            print('testb')
    
        def testc(self):
            print('testc')
    
    
    if __name__ == "__main__":
        unittest.main()

    执行结果如下:

    (二)配置文件:.ini、yaml、yml

    1、ini 文件是Initialization File的缩写,即初始化文件。一般可变的东西尽可能的放在配置文件当中,易于编辑修改。

    (1)配置文件:config.ini

    [redis]
    host = 127.0.0.1
    password = 123456
    port = 6379
    
    [mysql]
    host = 127.0.0.1
    password = 123456
    port = 6379
    user = root
    db = jxz
    
    [server]
    host = 127.0.0.1:8000

    (2)解析配置文件

    判断查找的节点存不存在:两种方法

    # 1、判断节点存不存在: c.sections() # 里面所有的节点
            if node in c.sections():
                 result = dict(c[node])
                 return result
    
    # 2、用try方法
            try:
                result = dict(c[node])
            except Exception as e:
                print("查找的节点不存在!")
            else:
                return result

    (3)所有的代码:解析配置文件.py

    import configparser
    import os
    
    # with open('config.ini',encoding='utf-8') as fr:
    #     c = configparser.ConfigParser()
    #     c.read_file(fr)
    #     result = dict(c['server'])
    #     print(result)
    
    # 定义函数
    def parse_ini(node,file_path='config.ini'):
        if not os.path.exists(file_path):
            raise Exception('ini文件不存在!')
    
        with open(file_path, encoding='utf-8') as fr:
            c = configparser.ConfigParser()
            c.read_file(fr)
            
            # 1、判断节点存不存在: c.sections() # 里面所有的节点
            if node in c.sections():
                result = dict(c[node])
                return result
    
            # 2、用try方法
            # try:
            #     result = dict(c[node])
            # except Exception as e:
            #     print("查找的节点不存在!")
            # else:
            #     return result
    
    
            result1= dict(c[node])
    
            print(result1)
    
    if __name__ == "__main__":
        redis_info=parse_ini('redis')
        print(redis_info)

    2、配置文件:yaml和yml:用于存储测试用例的数据

    (1)data.yaml

    name : 1
    port : 3306
    names :
          - body
          - eyes
          - hair

    (2)解析yaml文件,需要提前安装模块:

    pip install pyyaml

    (3)解析yaml.py

    import yaml
    
    with open('data.yaml',encoding='utf-8') as fr:
        print(yaml.load(fr,Loader=yaml.SafeLoader))
    
    if __name__ == "__main__":
        pass

    注:若遇到如下warning警告:

    YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.
    print(yaml.load(fr))

    需添加下面的代码:

    yaml.load(fr,Loader=yaml.SafeLoader

    (4)定义函数的代码:

    import yaml
    
    def load_yaml(file_path):
        with open(file_path,encoding='utf-8') as fr:
            return yaml.load(fr,Loader=yaml.SafeLoader)
    
    if __name__ == "__main__":
        result = load_yaml('data.yaml')
        print(result)

    (三)钉钉发送消息:加签和验签

    1、添加机器人:群设置-智能群助手-添加机器人

    (1)添加机器人时,安全设置可选择:自定义关键词 | 加签 | IP地址

    (2)安全设置选择:自定义关键词,利用postman进行发送消息,参考钉钉帮助文档:https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq/d535db33

     钉钉群获得的消息如下截图:

    2、安全设置选择加签方式,发送钉钉消息

    (1)加签,签名:把timestamp+" "+密钥当做签名字符串,使用HmacSHA256算法计算签名,然后进行Base64 encode,最后再把签名参数再进行urlEncode,得到最终的签名(需要使用UTF-8字符集)。

    (2)url编码进行加密和解密

    import urllib
    from urllib.parse import quote,unquote
    print(quote('hello-123_你好'))
    secret = 'hello-123_%E4%BD%A0%E5%A5%BD'
    print(unquote(secret))

    执行结果如下:

       (3)生成加密,使用postman工具进行发送消息

    import time
    import hashlib
    import base64
    import hmac
    from urllib.parse import quote
    timestamp = int(time.time() * 1000)
    secret='SECf2f4947ab1160ccdb6040a88edb91be0427835976aa6dee6eee0969XXXXXXXX'
    
    sign_before = '%s
    %s' % (timestamp,secret)
    hsha256 = hmac.new(secret.encode(),sign_before.encode(),hashlib.sha256)
    
    # sha256 = hashlib.sha256(sign_before.encode())
    sign_sha256 = hsha256.digest()
    sign_b64 =base64.b64encode(sign_sha256)
    sign = quote(sign_b64)
    print(timestamp,sign)

    执行结果如下:

     使用postman接口工具进行发送消息:

    (4) 直接编码发送钉钉消息

    import hmac
    import time
    import hashlib
    import base64
    from urllib.parse import quote
    import requests
    
    url = "https://oapi.dingtalk.com/robot/send?access_token=bd413385218506104a1903badd88016ba9ae9b7a1738bde9f7a3573aXXXXXXXX"
    def create_sign():
        secret = 'SECf2f4947ab1160ccdb6040a88edb91be0427835976aa6dee6eee09691xxxxxxxx'
        timestamp = int(time.time() * 1000)
        sign_before = '%s
    %s' % (timestamp,secret)
        hsha265 = hmac.new(secret.encode(),sign_before.encode(),hashlib.sha256)
        sign_sha256 = hsha265.digest()
        sign_b64 = base64.b64encode(sign_sha256)
        sign = quote(sign_b64)
        return {"timestamp":timestamp,"sign":sign}
    
    def send_msg_dingding(msg="happy everyday!"):
        data = {
            "msgtype": "text",
            "text": {
                "content": msg
            },
            "at": {
                "atMobiles": [
                    "1312007xxxx"
                ],
                "isAtAll": False
            }
        }
    
        sign = create_sign()
        r = requests.post(url,params = sign,json=data)
        print(r.json())
    
    if __name__ == "__main__":
        send_msg_dingding("好好过好每一天!")

    执行结果如下:

    (四)写自动化测试用例:Rainbow

    1、彩虹-主架构设计

     (1)分别创建目录,如下图:

    图A:

    图B:

    (2)config_parse.py

    import os
    import configparser
    import yaml
    from common.log import Log
    from config.settings import CONFIG_FILE,CASE_DATA_PATH
    
    def parse_ini(node,file_path=CONFIG_FILE):
        if not os.path.exists(file_path):
            Log.error("配置文件不存在,文件路径{}",file_path)
            raise Exception('ini文件不存在!')
    
        with open(file_path, encoding='utf-8') as fr:
            c = configparser.ConfigParser()
            c.read_file(fr)
    
            if node in c.sections():
                result = dict(c[node])
                return result
            Log.warning("配置文件中[{}]节点不存在",node)
    
    def load_yaml(file_name):
        file_path = os.path.join(CASE_DATA_PATH,file_name)
        if not os.path.exists(file_path):
            Log.error("用例数据文件不存在,文件路径{}",file_path)
            raise Exception('yaml文件不存在!')
    
        with open(file_path,encoding='utf-8') as fr:
            return yaml.load(fr,Loader = yaml.SaveLoader)
        
    if __name__ == "__main__":
        parse_ini("mysql",'mysql.ini')

    (3)log.py

    from loguru import logger
    import sys
    from config.settings import LOG_FILE,LOG_LEVEL
    class Log:
        logger.remove()
        fmt = '[{time}][{level}][{file.path}:line:{line}:function_name:{function}] || msg={message}'
        # level file function module time message
        logger.add(sys.stdout,level=LOG_LEVEL,format=fmt)
        logger.add(LOG_FILE,level=LOG_LEVEL,format=fmt,encoding='utf-8',enqueue=True,rotation='1 day',retention='10 days')
        debug = logger.debug
        info = logger.info
        warning = logger.warning
        error = logger.error
    
    
    if __name__ == "__main__":
        Log.info("日志测试")

    (4)operate_db.py

    import pymysql
    import traceback
    from common.log import Log
    
    class MySQL:
        def __init__(self,host,user,password,db,charset='utf8',autocommit=True,port=3306):
            port = int(port)
            self.conn = pymysql.connect(user=user,host=host,password=password,port=port,
                                        db=db,charset=charset,autocommit=autocommit)
            self.cursor = self.conn.cursor(pymysql.cursors.DictCursor)
            Log.info("开始连接mysql")
    
        def __del__(self):
            self.__close()
    
        def execute(self,sql):
            try:
                self.cursor.execute(sql)
            except Exception:
                Log.error('sql执行出错,sql语句是{}',sql)
                Log.error(traceback.format_exc())
    
        def fetchall(self,sql):
            self.execute(sql)
            return self.cursor.fetchall()
    
        def fetchone(self,sql):
            self.execute(sql)
            return self.cursor.fetchone()
    
        def bak_db(self):
            pass
    
        def __close(self):
            self.cursor.close()
            self.conn.close()
    
    if __name__ == "__main__":
        pass

    (5)config.ini

    [mysql]
    host=118.24.3.xx
    user=jxz
    password=123456
    db=jxz
    charset=utf8
    
    [mysql2]
    host=118.24.3.xxx
    user=jxz
    password=123456
    db=jxz
    charset=utf8
    port =3306
    
    
    [redis]
    host=118.24.3.40
    password=xxxx
    port=6379
    
    [dingding]
    url = https://oapi.dingtalk.com/robot/send
    secret = SECf2f4947ab1160xxxxxxxxxxedb91be0427835976aa6dee6eee096xxxxxxxxxx
    access_token = bd4133852xxxxxxxxxxxx3badd88016ba9ae9b7a1738bde9f7a3573axxxxxxx
    at = 1312007xxxx
    
    [mail]
    host=smap.qq.com
    user=127xxxx070@qq.com
    password=1962xxxxzh
    to=krystal_xiao@126.com
    asc=12xxxxx767@qq.com

    (6)settings.py

    import os
    
    BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    
    LOG_FILE = os.path.join(BASE_PATH,'logs','rainbow.log') # 日志文件
    
    REPORT_PATH = os.path.join(BASE_PATH,'report') # 报告存放的目录
    
    CASE_PATH = os.path.join(BASE_PATH,'biz','cases') # 测试用例的目录
    
    CASE_DATA_PATH = os.path.join(BASE_PATH,'biz','data') # 测试数据的目录
    
    CONFIG_FILE = os.path.join(BASE_PATH,'config','config.ini') # 配置文件的目录
    
    LOG_LEVEL = 'INFO' # 默认日志级别
    
    
    
    if __name__ == "__main__":
        pass

    (7)biz.support.urls.py

    from urllib.parse import urljoin
    from common.config_parse import parse_ini
    
    
    host = parse_ini('server').get('host')
    
    class ServerUrl:
        login_url =  urljoin(host,'/api/user/login') #登录url
        register_url =  urljoin(host,'/api/user/user_reg') #注册url
    
    
    
    if __name__ == "__main__":
        pass

    2、彩虹-MySQL连接

    (1)新建一个文件夹:

     (2)utils.py

    import redis
    import time
    import hmac
    import hashlib
    import base64
    from urllib.parse import quote
    from common.config_parse import parse_ini
    from common.operate_db import MySQL
    
    ddconfig = parse_ini('dingding') # 取钉钉的配置文件
    secret = ddconfig.get("secret")
    
    mysql_conn_mapper = {} #{mysqlNone:Mysql,mysql2None:Mysql2}
    redis_mapper = {}
    def get_mysql(node='mysql',db=None):
        key = '%s%s'%(node,db)
        if not key in mysql_conn_mapper:
            mysql_info = parse_ini(node)
            if db:
                mysql_info['db']=db
            mysql = MySQL(**mysql_info)
            mysql_conn_mapper[key] = mysql
        else:
            mysql = mysql_conn_mapper[key]
        return mysql
    
    def get_redis(node='redis',db=None):
        key = '%s%s'%(node,db)
        if not key in redis_mapper:
            redis_info = parse_ini(node)
            if db:
                redis_info['db'] = db
            r = redis.Redis(**redis_info)
            redis_mapper[key] = r
        else:
            r = redis_mapper[key]
        return r
    
    def create_sign():
        secret = 'SECf2f4947ab1160ccdbxxxxa88edb91be0427835976aa6dee6eee096912xxxxxx'
        timestamp = int(time.time() * 1000)
        sign_before = '%s
    %s' % (timestamp,secret)
        hsha265 = hmac.new(secret.encode(),sign_before.encode(),hashlib.sha256)
        sign_sha256 = hsha265.digest()
        sign_b64 = base64.b64encode(sign_sha256)
        sign = quote(sign_b64)
        return {"timestamp":timestamp,"sign":sign}
    
    
    if __name__ == "__main__":
        c = get_mysql()

    3、NbDict

    (1)新建一个文件目录

     (2)custom_class.py

    class NbDict(dict):
    
        def __getattr__(self, item): # {"login_time"}
            value = self.get(item)
            if type(value) == dict:
                value = NbDict(value)
    
            elif isinstance(value,list) or isinstance(value,tuple):
                value = list(value)
                for index,obj in enumerate(value):
                    if isinstance(obj,dict):
                        value[index] = NbDict(obj)
            return value
    if __name__ == "__main__":
        d = {"login_time":1}
        d1 = NbDict(d)
        print(d1.login_time2)

    4、彩虹-封装http请求类

    (2)新建一个文件目录:

    (2)http_request.py

    import requests
    import traceback
    from common.log import Log
    from common.utils import create_sign
    from common.custom_class import NbDict
    
    
    class Requests:
        def __init__(self,url,params=None,data=None,headers=None,json=None,files=None):
            self.url = url
            self.params = params
            self.data = data
            self.headers = headers
            self.json = json
            self.files = files
    
        def _get_response(self):
            try:
                result = self.req.json()
            except Exception as e:
                return self.req.text
            else:
                result = NbDict(result)
            return result
    
        def get(self):
            Log.info("开始发送get请求")
            Log.info("url:【{}】,params:【{}】,headers:{}",self.url,self.params,self.headers)
            try:
                self.req = requests.get(self.url,params=self.params,headers=self.headers,verify=False)
            except Exception as e:
                Log.error("http请求发送错误,错误信息:{}",traceback.format_exc())
                raise Exception("接口请求不通")
            else:
                return self._get_response()
    
        def post(self):
            Log.info("开始发送post请求")
            Log.info("url:【{}】,params:【{}】,headers:{},data:{},json:{},files:{}",
                     self.url, self.params, self.headers,self.data,self.json,self.files)
            try:
                self.req = requests.post(self.url,params=self.params,
                              data=self.data,json=self.json,
                              files=self.files, # {"key":open("f.py",'rb')}
                              headers=self.headers,verify=False)
            except Exception as e:
                Log.error("http请求发送错误,错误信息:{}", traceback.format_exc())
                raise Exception("接口请求不通")
            else:
                return self._get_response()
    
    if __name__ == "__main__":
        url = "https://oapi.dingtalk.com/robot/send?access_token=bd41xxxxxxxxxxx104a190xxxxx88016ba9ae9b7a1738bde9f7a3573axxxxxxxx"
        sign = create_sign()
        msg = "Good Evening!"
        data = {
            "msgtype": "text",
            "text": {
                "content": msg
            },
            "at": {
                "atMobiles": [
                    "13xxxxx1212"
                ],
                "isAtAll": False
            }
        }
        r = Requests(url,params=sign,json=data)
        result = r.post()
        print(result)

    5、彩虹-封装发送消息的方法

    (1)新建一个目录

    (2) 代码如下:send_msg.py

    import yamail
    from common.http_request import Requests
    from common.utils import parse_ini,create_sign
    ddconfig = parse_ini('dingding') # 取钉钉的配置信息
    mail_config =parse_ini('mail') # 取邮件的配置信息
    url = ddconfig.get('url') # 钉钉l
    access_token = ddconfig.get('access_token') # access_token
    at= ddconfig.get('at','').split(',') # 钉钉发送消息的时候at给谁
    
    def send_dingding(msg):
        data = {
            "msgtype": "text",
            "text": {
                "content": msg
            },
            "at": {
                "atMobiles": at,
                "isAtAll": False
            }
        }
        sign = create_sign()
        sign['access_token'] = access_token
        r = Requests(url,params=sign,json=data)
        r.post()
    
    def send_mail(subject,contents,attachments=None):
        smtp = yamail.SMTP(
            host=mail_config.get("host"),
            user = mail_config.get("user"),
            password =mail_config.get("password")
    
        )
        smtp.sent(to=mail_config.get("to",'').split(','), # 发送给谁
                subject = subject, # 邮件主题
                DD =mail_config.get("xxx",'').split(','), # 抄送
                contents=contents, # 邮件正文
                attachments=attachments #附件,如果是多个附件,写list
        )
        smtp.close()
    
    if __name__ == "__main__":
        send_dingding("good evening,guys!")

    6、彩虹-完成

    (1)在flow文件夹下面新建一个user.py文件

     代码如下:user.py

    from common.http_request import Requests
    from biz.support.urls import ServerUrl
    
    class UserRequest:
    
        @classmethod
        def login(cls,username,password):
            '''
            调用登录接口的
            :param username: 用户名
            :param password: 密码
            :return:
            '''
            data = {
                'username':username,
                'passwd':password
            }
            req = Requests(ServerUrl.login_url,data=data)
            return req.post()
    
        @classmethod
        def register(cls,username,password,cpassword):
            '''
            注册
            :param username: 用户名
            :param password: 密码
            :param cpassword: 确认密码
            :return:
            '''
            data = {
                'username': username,
                'pwd': password,
                'cpwd':cpassword
            }
            req = Requests(ServerUrl.register_url,data=data)
            return req.post()
    
    if __name__ == "__main__":
        result = UserRequest.login('niuhanyang','aAxxxxxx')
        print(result)

    执行结果如下:

    (2)在data文件夹下面新建一个login_data.yaml文件:

     yaml文件信息如下:

    username : niuhanyang
    password : xxxxxxx

    (3)在cases文件夹下新建3个文件,分别为:base_case.py、test_login.py、test_open_acc.py

    代码如下:

    base_case.py

    import unittest
    from common.config_parse import load_yaml
    
    class BaseCase(unittest.TestCase):
        data_file_name = None
    
        @property
        def file_data(self):
            data = load_yaml(self.data_file_name)
            return data
    
        @classmethod
        def get_token(cls,username):
            pass
    
    
    if __name__ == "__main__":
        pass

    test_login.py:

    import unittest
    from biz.flow.user import UserRequest
    from common.utils import get_redis,get_mysql
    from common.config_parse import load_yaml
    from biz.cases.base_case import BaseCase
    
    class TestLogin(BaseCase):
        '''登录接口测试用例'''
        data_file_name = 'login_data.yaml'
    
        @classmethod
        def setUpClass(cls):
            cls.redis = get_redis()
            cls.mysql = get_mysql()
            # cls.file_data = load_yaml('login_data.yaml') 
    
        def test_normal(self):
            '''正常登录'''
            username = self.file_data.get('username')
            password = self.file_data.get('password')
    
            ret = UserRequest.login(username,password)
            self.assertEqual(0,ret.error_code,'返回的错误码不是0')
            self.assertIsNotNone(ret.login_info.login_time,msg='logintime为空')
            redis_key = 'session:%s' % username
            sessionid = self.redis.get(redis_key)
            sql = 'select id from app_myuser where username = "%s";' % username
            db_result = self.mysql.fetchone(sql)
            user_id = db_result.get('id')
            self.assertEqual(sessionid,ret.login_info.sign,msg="返回的session和Redis中的不一致")
            self.assertEqual(user_id,ret.login_info.userId,msg="返回的userId和数据库中的不一致")
    
    
    
    if __name__ == "__main__":
        pass

    test_open_acc.py:

    import unittest
    from common.config_parse import load_yaml
    
    class BaseCase(unittest.TestCase):
        data_file_name = None
    
        @property
        def file_data(self):
            data = load_yaml(self.data_file_name)
            return data
    
        @classmethod
        def get_token(cls,username):
            pass
    
    
    if __name__ == "__main__":
        pass

    (4)执行run.py文件

    import unittest
    import os,time,sys
    
    BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    sys.path.index(0,BASE_PATH)
    
    from config.settings import CASE_PATH,REPORT_PATH,dd_template,mail_template
    from common import send_msg
    from common.HTMLTestRunner import HTMLTestRunner
    
    def run():
        test_suite = unittest.defaultTestLoader.discover(CASE_PATH,'test*.py')
        file_name = 'report_%s.html' % time.strftime('%Y%m%d%H%M%S')
        file_abs = os.path.join(REPORT_PATH,file_name)
        with open(file_abs,'wb') as fw:
            runner = HTMLTestRunner(stream=fw,title='测试报告标题',description='描述')
            case_result = runner.run(test_suite)
            all_count = case_result.failure_count + case_result.success_count
            dd_msg = dd_template % (all_count,case_result.success_count,case_result.failure_count)
            mail_msg = mail_template % (all_count, case_result.success_count, case_result.failure_count)
            send_msg.send_dingding(dd_msg)
            subject = '天马座自动化测试报告-%s' % time.strftime('%Y-%m-%d %H:%M:%S')
            send_msg.send_mail(subject,mail_msg,file_abs)
    
    if __name__ == '__main__':
        run()

    最后的执行结果:

     7、自动化测试用例流程:

    温故而知新
  • 相关阅读:
    Android:简单联网获取网页代码
    nginx搭建前端项目web服务器以及利用反向代理调试远程后台接口
    ElementUI使用问题记录:设置路由+iconfont图标+自定义表单验证
    vue中引入第三方字体图标库iconfont,及iconfont引入彩色图标
    Axios使用文档总结
    使用node中的express解决vue-cli加载不到dev-server.js的问题
    Vue脚手架(vue-cli)搭建和目录结构详解
    JS夯实基础:Javascript 变态题解析 (下)
    理解JS里的稀疏数组与密集数组
    JS夯实基础:Javascript 变态题解析 (上)
  • 原文地址:https://www.cnblogs.com/krystal-xiao/p/13894163.html
Copyright © 2020-2023  润新知