飞道的博客

python+unittest框架 UI自动化设计思路以及代码剖析,增加易用性

390人阅读  评论(0)

前言

学习UI自动化的同学都应该知道PO模式,PO共分为三层,分别是页面定位层,页面对象层,业务逻辑层。

po模式有以下几个优点

1.易读性好

2.扩展性高

3.复用性强

4.维护性好

5.代码冗余率低

前因:让不会代码的同学也能编写自动化
思考问题:市面上不乏有录制回放,数据驱动的框架,为什么还要自己封装呢
解决问题:封装能更加贴切自己公司的项目,能更好的进行扩展,而且更能展示自身的价值

这里我就不具体讲解selenium基础方法的封装了,和PO模式一样的,没有做很大的改动

源码:https://download.csdn.net/download/qq_36076898/15724810

一.用例设计(Excel)

1.Excel sheet设计

参数说明:用于生成测试数据,数据生成通过tool自动生成。后面会具体讲解怎么使用


定位:所有的元素定位。后面会具体讲解怎么使用

登录信息配置:环境、账号的配置。后面会具体讲解怎么使用

用例:测试用例,我这里就只写了个demo。后面会仔细剖析用例

二.代码封装

1.封装读取【参数说明】sheet的代码

import random
import string
import time
import datetime


class Tool:

    @staticmethod
    def get_random_str(random_length=6):
        """
        生成一个指定长度的随机字符串,数字字母混合
        string.digits=0123456789
        string.ascii_letters=abcdefghigklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
        """
        str_list = [random.choice(string.digits + string.ascii_letters) for i in range(random_length)]
        random_str = ''.join(str_list)
        return random_str

    @staticmethod
    def get_random_letters(random_length=6):
        """
        生成一个指定长度的随机字符串,纯字母
        string.ascii_letters=abcdefghigklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
        """
        str_list = [random.choice(string.ascii_letters) for i in range(random_length)]
        random_letters = ''.join(str_list)
        return random_letters

    @staticmethod
    def get_time_stamp(unit='s'):
        """
        获取时间戳
        :param unit: 单位   s 秒,ms 毫秒
        :return:
        """
        data = time.time()
        if unit == 's':
            result = int(data)
        elif unit == 'ms':
            result = int(round(data * 1000))
        else:
            result = data

        return result

    @staticmethod
    def get_date():
        """
        获取当前时间
        :return:
        """
        result = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        return result

    @staticmethod
    def get_phone_number(key=False):
        """
        随机生成电话号码
        :param key: 默认生成虚假手机号,True 生成真实手机号
        :return:
        """
        pre_list = ["130", "131", "132", "133", "134", "135", "136", "137", "138", "139",
                    "147", "150", "151", "152", "153", "155", "156", "157", "158", "159",
                    "186", "187", "188", "189"]
        if key:
            result = random.choice(pre_list) + "".join(random.choice("0123456789") for i in range(8))
        else:
            result = random.choice(pre_list) + "".join(random.choice("0123456789") * 4) + "".join(
                random.choice("0123456789") * 4)
        return result

    @staticmethod
    def get_email():
        """生成随机邮箱"""
        # 用数字0-9 和字母a-z 生成随机邮箱。
        list_sum = [i for i in range(10)] + ["a", "b", "c", "d", "e", "f", "g", "h", 'i', "j", "k",
                                             "l", "M", "n", "o", "p", "q", "r", "s", "t", "u", "v",
                                             "w", "x", "y", "z"]
        email_str = ""
        # email_suffix = ["@163.com", "@qq.com", "@gmail.com", "@mail.hk.com", "@yahoo.co.id", "mail.com"]
        email_suffix = ["@163.com", "@qq.com"]
        for i in range(10):
            a = str(random.choice(list_sum))
            email_str = email_str + a
        # 随机拼接不同的邮箱后缀
        return email_str + random.choice(email_suffix)

    @staticmethod
    def get_parameter(data):
        """
        判断某一条excel数据中书否存在参数,若存在直接替换掉
        :param data:
        :return:
        """
        value = data.replace('#', '')
        if 'get_time_stamp_ms' in value:
            test_data = getattr(Tool, value.replace('_ms', ''))('ms')
        elif 'get_time_stamp_s' in value:
            test_data = getattr(Tool, value.replace('_second', ''))()
        else:
            if hasattr(Tool, value):
                test_data = getattr(Tool, value)()
            else:
                test_data = value

        return test_data


if __name__ == '__main__':
    test = Tool()
    print(test.get_parameter('#get_time_stamp_second#'))

这里主要讲解下get_parameter()方法,其他方法都是生成数据的。get_parameter()方法处理用例中传的参数,根据传方法名生成想要的数据。例如:测过程中需要时间格式的数据,那么在测试用例中就可以填写:#get_date#

2.封装读取【定位】sheet的代码

import xlrd
from common.log import Log

log = Log()


class ReadExcel:

    def __init__(self, fileName):
        """
         new_data是最后返回的值
         config.project_path.replace('\\', '/') + "/data.xlsx"
        :param fileName: excel文件名路径
        """
        self.fileName = fileName
        # 读取excel文件夹
        self.book = xlrd.open_workbook(self.fileName)

    @staticmethod
    def data_type(test_type, test_value):
        """
        判断从excel单元格中获取的数据类型
        1 string(text), 2 number, 3 date, 4 boolean
        :param test_type: 类型
        :param test_value: 值
        :return:
        """
        if test_type == 1:
            """字符串"""
            return test_value

        elif test_type == 2:
            if '.0' in str(test_value):
                """整数"""
                return int(test_value)
            else:
                """浮点"""
                return test_value

        elif test_type == 3:
            """日期"""
            date = xlrd.xldate_as_datetime(test_value, 0).strftime('%Y-%m-%d')
            return date

        elif test_type == 4:
            """布尔类型"""
            if test_value == 1:
                return True
            elif test_value == 0:
                return False

    def stitching_element(self, sheet_name='定位'):
        """
        读取excel,处理元素数据
        :param sheet_name:
        :return:
        """
        result = {
   }
        # 读取excel
        sheet = self.book.sheet_by_name(sheet_name)
        row_value_one = sheet.row_values(0)  # 第一行
        # 总行数
        row_num = sheet.nrows
        for i in range(1, row_num):
            result[sheet.cell_value(i, row_value_one.index('功能'))] = sheet.cell_value(i, row_value_one.index('元素定位'))
        return result

结果如下:返回字典,【功能】作为key,【元素定位】作为vlaue

{
   '登录-用户名': '//*[@placeholder="请输入内容"]', '登录-密码': '//*[@placeholder="请输入密码"]',
             '登录-登录按钮': '//span[text()=" 登录 "]', '登录-错误提示': '//p[text()="用户名或者密码错误"]',
             '首页-昵称': '//*[@id="app"]/section/header/div[2]/span', '首页-退出登录': '//li[text()="退出登录"]',
             '首页-一级菜单': '//li[@role="menuitem"]/div/span', '首页-用户管理': '//span[text()="用户管理"]',
             '首页-用户管理-用户列表': '//li/span[text()="用户列表"]', '首页-用户管理-用户列表-添加用户': '//button/span[text()="添加用户"]',
             '首页-用户管理-用户列表-添加用户-用户名': '//label[text()="用户名"]/following-sibling::div[1]/div/input',
             '首页-用户管理-用户列表-添加用户-昵称': '//label[text()="昵称"]/following-sibling::div[1]/div/input',
             '首页-用户管理-用户列表-添加用户-邮箱': '//label[text()="邮箱"]/following-sibling::div[1]/div/input',
             '首页-用户管理-用户列表-添加用户-手机号': '//label[text()="手机号"]/following-sibling::div[1]/div/input',
             '首页-用户管理-用户列表-添加用户-密码': '//label[text()="密码"]/following-sibling::div[1]/div/input',
             '首页-用户管理-用户列表-添加用户-确定': '//*[@id="app"]/section/section/main/div/div[4]/div/div[4]/div/div[3]/span/button[2]/span',
             '首页-用户管理-用户列表-当前页的所有用户': '//tr/td[2]/div'}

3.封装读取【登录信息配置】sheet的代码

import xlrd
from common import config
from common.log import Log

log = Log()


class ReadExcel:

    def __init__(self, fileName):
        """
         new_data是最后返回的值
         config.project_path.replace('\\', '/') + "/data.xlsx"
        :param fileName: excel文件名路径
        """
        self.fileName = fileName
        # 读取excel文件夹
        self.book = xlrd.open_workbook(self.fileName)

    @staticmethod
    def data_type(test_type, test_value):
        """
        判断从excel单元格中获取的数据类型
        1 string(text), 2 number, 3 date, 4 boolean
        :param test_type: 类型
        :param test_value: 值
        :return:
        """
        if test_type == 1:
            """字符串"""
            return test_value

        elif test_type == 2:
            if '.0' in str(test_value):
                """整数"""
                return int(test_value)
            else:
                """浮点"""
                return test_value

        elif test_type == 3:
            """日期"""
            date = xlrd.xldate_as_datetime(test_value, 0).strftime('%Y-%m-%d')
            return date

        elif test_type == 4:
            """布尔类型"""
            if test_value == 1:
                return True
            elif test_value == 0:
                return False

    def processing_environment(self, sheet_name='登录信息配置'):
        """处理环境和账号"""
        result = {
   }
        sheet = self.book.sheet_by_name(sheet_name)
        cell_values = sheet.merged_cells
        row_value_one = sheet.row_values(3)
        cell_range = []
        for cell_value in cell_values:
            for i in range(cell_value[0], cell_value[1]):
                for j in range(cell_value[2], cell_value[3]):
                    if '是' == sheet.cell_value(i, j):
                        cell_range.append(cell_value)
                        break
        result['url'] = sheet.cell_value(cell_range[0][0], row_value_one.index('url'))
        for i in range(cell_range[0][0], cell_range[0][1]):
            if i == cell_range[0][0]:
                result['account'] = {
   
                    "username": sheet.cell_value(i, row_value_one.index('用户名')),
                    "nickname": sheet.cell_value(i, row_value_one.index('昵称')),
                    "password": self.data_type(2, sheet.cell_value(i, row_value_one.index('密码')))
                }
            result[sheet.cell_value(i, row_value_one.index('用户名'))] = {
   
                "nickname": sheet.cell_value(i, row_value_one.index('昵称')),
                "password": self.data_type(2, sheet.cell_value(i, row_value_one.index('密码')))
            }
        result['username_text'] = sheet.cell_value(0, 1)
        result['password_text'] = sheet.cell_value(1, 1)
        result['login_button'] = sheet.cell_value(2, 1)
        return result

结果如下:

{
   
'url': 'http://127.0.0.1/#/login', 
'account': {
   'username': 'test1', 'nickname': '测试', 'password': 123456},
'test1': {
   'nickname': '测试', 'password': 123456}, 
'root': {
   'nickname': '超级管理员', 'password': 123456},
'username_text': '登录-用户名', 
'password_text': '登录-密码', 
'login_button': '登录-登录按钮'
}

4.封装读取【demo】(用例)sheet的代码

import xlrd
from common import config
from common.log import Log

log = Log()


class ReadExcel:

    def __init__(self, fileName):
        """
         new_data是最后返回的值
         config.project_path.replace('\\', '/') + "/data.xlsx"
        :param fileName: excel文件名路径
        """
        self.fileName = fileName
        # 读取excel文件夹
        self.book = xlrd.open_workbook(self.fileName)

    @staticmethod
    def data_type(test_type, test_value):
        """
        判断从excel单元格中获取的数据类型
        1 string(text), 2 number, 3 date, 4 boolean
        :param test_type: 类型
        :param test_value: 值
        :return:
        """
        if test_type == 1:
            """字符串"""
            return test_value

        elif test_type == 2:
            if '.0' in str(test_value):
                """整数"""
                return int(test_value)
            else:
                """浮点"""
                return test_value

        elif test_type == 3:
            """日期"""
            date = xlrd.xldate_as_datetime(test_value, 0).strftime('%Y-%m-%d')
            return date

        elif test_type == 4:
            """布尔类型"""
            if test_value == 1:
                return True
            elif test_value == 0:
                return False

    def stitching_data(self, case_type='冒烟'):
        """
        读取excel,处理用例数据
        :return:
        """
        result = []
        # 获取所有sheet
        sheets = self.book.sheet_names()
        for sheet_name in sheets:
            if sheet_name != '参数说明' and sheet_name != '定位' and sheet_name != '登录信息配置':
                sheet = self.book.sheet_by_name(sheet_name)
                # 用例数量
                case_num_list = sheet.merged_cells[0:len(sheet.merged_cells) // 3]
                # 获取第行列数据
                row_value_one = sheet.row_values(0)  # 第一行
                # 处理数据
                for i in case_num_list:
                    tag = sheet.cell_value(i[0], row_value_one.index('用例类型'))
                    if case_type == tag or case_type is True:
                        case = {
   'description': sheet.cell_value(i[0], row_value_one.index('描述')), 'tag': tag}
                        step = []
                        for j in range(i[0], i[1]):
                            step.append(
                                {
   'element': sheet.cell_value(j, row_value_one.index('定位')),
                                 'operate': sheet.cell_value(j, row_value_one.index('操作方式')),
                                 'data': sheet.cell_value(j, row_value_one.index('测试数据')),
                                 'result': sheet.cell_value(j, row_value_one.index('参数标签'))
                                 })
                        case['step'] = step
                        result.append(case)
        return result
[
{
   'description': '验证超级管理员能否创建用户',
 'tag': '冒烟',
 'step': [
 		{
   'element': '', 'operate': '登录', 'data': '#root#', 'result': ''}, 
 		{
   'element': '首页-用户管理', 'operate': '单击', 'data': '', 'result': ''}, 
 		{
   'element': '首页-用户管理-用户列表', 'operate': '单击', 'data': '', 'result': ''}, 		{
   'element': '首页-用户管理-用户列表-添加用户', 'operate': '单击', 'data': '', 'result': ''}, {
   'element': '首页-用户管理-用户列表-添加用户-用户名', 'operate': '输入', 'data': '#get_random_letters#', 'result': '期望结果'}, 
 		{
   'element': '首页-用户管理-用户列表-添加用户-昵称', 'operate': '输入', 'data': '#get_random_letters#', 'result': ''}, 
 		{
   'element': '首页-用户管理-用户列表-添加用户-邮箱', 'operate': '输入', 'data': '#get_email#', 'result': ''}, 
 		{
   'element': '首页-用户管理-用户列表-添加用户-手机号', 'operate': '输入', 'data': '#get_phone_number#', 'result': ''}, 
 		{
   'element': '首页-用户管理-用户列表-添加用户-密码', 'operate': '输入', 'data': '#get_random_letters#', 'result': ''}, 
 		{
   'element': '首页-用户管理-用户列表-添加用户-确定', 'operate': '单击', 'data': '', 'result': ''}, 
 		{
   'element': '首页-用户管理-用户列表-当前页的所有用户', 'operate': '获取多个文本', 'data': '', 'result': '实际结果'},
 		{
   'element': '', 'operate': '期望结果in实际结果', 'data': '', 'result': ''}]
 		 },
 		 
{
   'description': '验证超级管理员能否创建用户', 
'tag': '冒烟', 
'step': [
		{
   'element': '', 'operate': '登录', 'data': '#root#', 'result': ''},
		{
   'element': '首页-用户管理', 'operate': '单击', 'data': '', 'result': ''},
		{
   'element': '首页-用户管理-用户列表', 'operate': '单击', 'data': '', 'result': ''},
		{
   'element': '首页-用户管理-用户列表-添加用户', 'operate': '单击', 'data': '', 'result': ''}, 
		{
   'element': '首页-用户管理-用户列表-添加用户-用户名', 'operate': '输入', 'data': '#get_random_letters#', 'result': '期望结果'}, 
		{
   'element': '首页-用户管理-用户列表-添加用户-昵称', 'operate': '输入', 'data': '#get_random_letters#', 'result': ''}, 
		{
   'element': '首页-用户管理-用户列表-添加用户-邮箱', 'operate': '输入', 'data': '#get_email#', 'result': ''}, 
		{
   'element': '首页-用户管理-用户列表-添加用户-手机号', 'operate': '输入', 'data': '#get_phone_number#', 'result': ''}, 
		{
   'element': '首页-用户管理-用户列表-添加用户-密码', 'operate': '输入', 'data': '#get_random_letters#', 'result': ''}, 
		{
   'element': '首页-用户管理-用户列表-添加用户-确定', 'operate': '单击', 'data': '', 'result': ''}, 
		{
   'element': '首页-用户管理-用户列表-当前页的所有用户', 'operate': '获取多个文本', 'data': '', 'result': '实际结果'}, 
		{
   'element': '', 'operate': '期望结果in实际结果', 'data': '', 'result': ''}
		]
		}
]

三.用例剖析以及设计思路讲解


代码会自动遍历获取所有sheet,【参数说明】【定位】【登录信息配置】除外,将所获取到的用例进行数据组装。


用例步骤:这个应该都会写,具体到每一步做什么。最后需要加一步这条用例通过的判断标准即可

操作方式:对应步骤所作出的操作,比如:单击、输入等,主要说一下第一步和最后一步所对应的操作。第一步操作如果是【登录】,操作方式直接填【登录】(这里登录有三步,输入用户名、输入密码、点击登录,我直接封装在代码里了,减少excel重复添加。如果有验证码,可以自行添加封装);最后一步通过方式必须按照【期望结果in实际结果】此格式书写,【期望结果in实际结果】代表期望结果在实际结果中,【期望结果=实际结果】代表期望结果等于实际结果

定位:根据【定位】sheet中【功能】写

测试数据:某一步骤需要说明数据,直接填写对应的方法即可,参考【参数说明】sheet;也可直接填写【参数标签】里所需要的参数

参数标签:主要用户获取期望结果、实际结果、后面步骤可能需要之前某个步骤的数据。

用例类型:方便执行脚本的时候区分执行那类用例

import ddt
import unittest
from common.log import Log
from common.read_excel import ReadExcel
from common.base import Base
from common.tool import Tool
from runTest import run_parser

tool = Tool()
log = Log()
case_info = run_parser()
read_excel = ReadExcel(case_info['file_name'])
case = case_info['case']
login_info = read_excel.processing_environment()


@ddt.ddt
class TestAllCase(unittest.TestCase):

    @classmethod
    def setUpClass(cls) -> None:
        log.info('<------------------用例执行开始------------------>')

    def setUp(self) -> None:
        self.case_name = self._testMethodName  # 获取执行当前用例的 方法名
        self.case = Base()
        log.info('--------开始{}用例--------'.format(self.case_name))

    def tearDown(self) -> None:
        self.case.quit()
        log.info('--------结束{}用例--------\n'.format(self.case_name))

    @classmethod
    def tearDownClass(cls) -> None:
        log.info('<------------------用例执行结束------------------>')

    @ddt.data(*case)
    def test_speed(self, data):
        log.info(data['description'])
        test_data = {
   }  # 测试过程中需要的数据
        desired_result = []  # 期望结果
        actual_result = []  # 实际结果
        for i in data['step']:
            if i['operate'] == '登录':
                # 登录
                self.case.open(login_info['url'])
                if i['data'] == '':
                    self.case.send_keys(element=login_info['username_text'],
                                        text=login_info['account']['username'])
                    self.case.send_keys(element=login_info['password_text'],
                                        text=login_info['account']['password'])
                else:
                    self.case.send_keys(element=login_info['username_text'],
                                        text=i['data'].replace('#', ''))
                    self.case.send_keys(element=login_info['password_text'],
                                        text=login_info[i['data'].replace('#', '')]['password'])
                self.case.click(element=login_info['login_button'])

            elif '期望结果' in i['operate'] or '实际结果' in i['operate']:
                # 断言
                log.info("期望结果:{}".format(desired_result[0]))
                log.info("实际结果:{}".format(actual_result[0]))
                if '=' in i['operate']:
                    self.assertEqual(desired_result[0], actual_result[0])
                elif 'in' in i['operate']:
                    self.assertTrue(desired_result[0] in actual_result[0])

            else:
                # 其他操作
                if i['operate'] == '单击':
                    self.case.click(element=i['element'])

                elif i['operate'] == '输入':
                    input_data = tool.get_parameter(i['data'])
                    if input_data in test_data.keys():
                        self.case.send_keys(element=i['element'], text=test_data[input_data])
                    else:
                        self.case.send_keys(element=i['element'], text=input_data)
                        if i['result'] != '' and i['result'] != '期望结果':
                            # 将复用的参数加入
                            test_data[i['result']] = input_data
                        elif i['result'] == '期望结果':
                            # 将期望结果加入
                            desired_result.append(input_data)

                elif i['operate'] == '获取多个文本':
                    actual_result.append(self.case.get_texts(element=i['element']))

                elif i['operate'] == '获取单个文本':
                    actual_result.append(self.case.get_text(element=i['element']))

                elif i['operate'] == '悬停':
                    self.case.mouse_flight(element=i['element'])

四.批量运行生成报告

import sys
import unittest
import os

sys.path.append('../')
from common.log import Log
from common import config
from common.HTMLTestRunner_cn import HTMLTestRunner
from common.Email import SendMail
from common.read_excel import ReadExcel
import argparse

log = Log()
send_report_path = config.send_report_path
reportTitle = config.reportTitle
description = config.description


def run_parser():
    """参数运行"""
    parser = argparse.ArgumentParser(description='通过cmd传入参数执行')
    parser.add_argument('-t', '--case', default=True, help='用例类型')
    parser.add_argument('-f', '--fileName', default=config.project_path.replace('\\', '/') + "/data.xlsx", help='用例类型')
    args_case = parser.parse_args()
    case_type = args_case.case
    file_name = args_case.fileName
    read_excel = ReadExcel(fileName=file_name)
    case_info = read_excel.stitching_data(case_type)
    return {
   'case': case_info, "file_name": file_name}


def add_case():
    """加载所有的测试用例"""
    # test_suite = unittest.defaultTestLoader.discover(start_dir=path, pattern=pattern, top_level_dir=None)
    test_cases = unittest.TestSuite()
    discover = unittest.defaultTestLoader.discover(start_dir=config.case_path, pattern='test*.py')
    for test_suite in discover:
        for test_case in test_suite:
            # 添加用例到test_cases
            test_cases.addTests(test_case)

    log.info('测试用例总数:{}'.format(test_cases._tests.__len__()))
    return test_cases


def run_html(test_suit):
    """执行测试用例,生成报告"""
    # 判断存储报告的路径是否存在
    if os.path.exists(config.report_path) is False:
        os.makedirs(config.report_path)
    with open(send_report_path, 'wb') as f:
        runner = HTMLTestRunner(stream=f, title=reportTitle,
                                description=description,
                                verbosity=2, retry=1, save_last_try=True)
        runner.run(test_suit)
    SendMail().send()


def run():
    """运行"""
    # 运行命令:python runTest.py  -c 功能 -f excel文件路径
    cases = add_case()
    run_html(cases)


if __name__ == '__main__':
    run()

1.命令运行指定类型用例

python runTest.py  -t 冒烟 -f C:\Users\admin\Desktop\data.xlsx

-t:用例类型
-f:excel路径

上面命令含义:读取C:\Users\admin\Desktop\data.xlsx用例,执行冒烟测试用例

2.命令所有类型用例

默认执行全部用例

python runTest.py  -f C:\Users\admin\Desktop\data.xlsx

五.调试用例

1.将用例里的任务类型随便写一个关键字:test

2.将run文件里的代码作出相应修改

3.执行run()方法

后言

以上就是我的设计思路,如若有什么问题欢迎给位留言!目前只封装了几个常见的操作,后面若需要其他操作的时候再封装!


转载:https://blog.csdn.net/qq_36076898/article/details/114533642
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场