0


Python+Selenium+Pytest+Allure自动化测试框架实战实例(示例为我司网盘产品)

前言

应公司要求,组织员工培训自动化测试,所以也趁此机会把我所学习的自动化框架整理一下,虽说不是很完美,但也有所收获。

环境准备

序号库、插件、工具版本号1Python3.112Pycharm22.2.33pytest7.2.04pywin323055selenium34.6.06openpyxl3.0.107Chromedriver与当前浏览器版本对应即可8allure2.20.1

项目简介

测试地址

由于是公司内部产品,外部访问不了,这里不做说明,大家想尝试可以选择其他网站地址即可

测试范围

1、网盘的登录功能测试-验证正确帐号密码登录成功-验证错误用户名密码登录失败(有很多情况,用例里面做了充分的校验)
2、创建文件夹功能测试-文件夹名称编辑框测试

项目设计

1.python编程语言设计测试脚本

2.webdriver驱动浏览器并操作页面元素

3.二次封装webdriver Api 操作方法

4.采用PageObject设计模式,设计测试业务流程

5.通过UI对象库存储页面操作元素

6.通过数据文件存储数据,读取数据,参数化测试用例并驱动测试执行

7.通过第三方插件allure生成测试报告

目录结构

在这里插入图片描述

代码实现

1、首先我们需要封装一些常用方法,比如键盘操作、剪切板操作、解析Excel文件、读取配置文件、生成日志的方法等等,这里我们一一列举。

clipboard.py(设计剪贴板操作)
import win32con
import win32clipboard as WC

classClipBoard(object):'''设置剪切板内容和获取剪切板内容'''@staticmethoddefgetText():'''获取剪切板内容'''
        WC.OpenClipboard()
        value = WC.GetClipboardData(win32con.CF_TEXT)
        WC.CloseClipboard()return value

    @staticmethoddefsetText(value):'''设置剪切板内容'''
        WC.OpenClipboard()
        WC.EmptyClipboard()
        WC.SetClipboardData(win32con.CF_UNICODETEXT, value)
        WC.CloseClipboard()if __name__ =='__main__':pass
keyboard.py(键盘操作)
import win32api
import win32con
import time

classKeyBoard(object):'''模拟按键'''# 键盘码
    vk_code ={'enter':0x0D,'tab':0x09,'ctrl':0x11,'v':0x56,'a':0x41,'x':0x58,'c':67,"r":82,'down':40,'esc':27,'del':46,'left':37,'right':39,'Up':38,'space':32,'F5':116,}@staticmethoddefmultiple_go_down(num, name):# 多次按下同一个键
        t =1whileTrue:if t <= num:
                KeyBoard().keyDown(name)
                t +=1
                time.sleep(0.5)else:break@staticmethoddefkeyDown(key_name):"""按下键"""
        key_name = key_name.lower()try:
            win32api.keybd_event(KeyBoard.vk_code[key_name],0,0,0)except Exception as e:print('未按下enter键')print(e)@staticmethoddefkeyUp(key_name):"""抬起键"""
        key_name = key_name.lower()
        win32api.keybd_event(KeyBoard.vk_code[key_name],0, win32con.KEYEVENTF_KEYUP,0)@staticmethoddefoneKey(key):"""模拟单个键"""
        key = key.lower()
        KeyBoard.keyDown(key)
        time.sleep(0.1)
        KeyBoard.keyUp(key)@staticmethoddeftwoKeys(key1, key2):"""模拟组合键"""
        key1 = key1.lower()
        key2 = key2.lower()
        KeyBoard.keyDown(key1)
        KeyBoard.keyDown(key2)
        KeyBoard.keyUp(key1)
        KeyBoard.keyUp(key2)if __name__ =='__main__':pass
mylog.py(生成操作日志)
# -*- coding: utf-8 -*-import logging
import os
import time
import config.conf

classMyLog(object):def__init__(self, logger=None):"""
        phone_model 为手机型号
        指定保存日志的文件路径,日志级别,以及调用文件
        将日志存入到指定的文件中
        """# 日志文件夹,如果不存在则自动创建
        cur_path = config.conf.cur_path
        log_path = os.path.join(os.path.dirname(cur_path),f'Logs')ifnot os.path.exists(log_path):
            os.makedirs(log_path)# log 日期文件夹
        now_date = time.strftime('%Y-%m-%d', time.localtime(time.time()))
        phone_log_path = os.path.join(os.path.dirname(cur_path),f'Logs\\{now_date}')ifnot os.path.exists(phone_log_path):
            os.mkdir(phone_log_path)# 创建一个logger
        self.logger = logging.getLogger(logger)
        self.logger.setLevel(logging.INFO)# 创建一个handler,用于写入日志文件
        now_time = time.strftime('%Y%m%d-%H%M%S', time.localtime(time.time()))
        log_name = os.path.join(phone_log_path,f'{now_time}.log')
        fh = logging.FileHandler(log_name)
        fh.setLevel(logging.INFO)# 再创建一个handler,用于输出到控制台
        ch = logging.StreamHandler()
        ch.setLevel(logging.INFO)# 定义handler的输出格式
        formatter = logging.Formatter('%(asctime)s - %(levelname)s %(filename)s [line:%(lineno)d]: %(message)s')
        fh.setFormatter(formatter)
        ch.setFormatter(formatter)# 给logger添加handler
        self.logger.addHandler(fh)
        self.logger.addHandler(ch)defgetLog(self):return self.logger

通过测试项目设计,我们需要把测试数据存放在Excel文件中,把页面操作元素存在UI对象库中也就是一个配置文件,那么我们需要对Excel 和 ini文件解析,因此我们开始编写这两个方法,设计UI对象库和测试数据文件。

parseConfile.py(读取配置文件的方法)
import configparser
from config.conf import CONF_PATH

classParseConFile(object):def__init__(self):
        self.file= CONF_PATH
        self.conf = configparser.ConfigParser()
        self.conf.read(self.file, encoding='utf-8')# print(self.conf.read(self.file, encoding='utf-8'))# print(12)defget_all_sections(self):"""获取所有的section,返回一个列表"""return self.conf.sections()defget_all_options(self, section):"""获取指定section下所有的option, 返回列表"""return self.conf.options(section)defget_locators_or_account(self, section, option):"""获取指定section, 指定option对应的数据, 返回元祖和字符串"""try:
            locator = self.conf.get(section, option)if('->'in locator):
                locator =tuple(locator.split('->'))return locator
        except configparser.NoOptionError as e:print('error:', e)return'error: No option "{}" in section: "{}"'.format(option, section)defget_option_value(self, section):"""获取指定section下所有的option和对应的数据,返回字典"""
        value =dict(self.conf.items(section))return value

    defget_option_appointed_int(self, section, option):"""获取指定section下的指定option下的对应数据,返回int"""try:
            locator = self.conf.get(section, option)if('='in locator):# locator = int(locator.split('='))
                locator =int(locator.split('='))# print(locator)print("+++++++++++++++")return locator
        except configparser.NoOptionError as e:print('error:', e)return'error: No option "{}" in section: "{}"'.format(option, section)if __name__ =='__main__':pass
parseExcelFile.py(读取Excel表格的方法)
from openpyxl import load_workbook
from config.conf import DATA_Path

classParseExcel(object):def__init__(self):
        self.wk = load_workbook(DATA_Path)
        self.excelFile = DATA_Path

    defget_sheet_by_name(self, sheet_name):"""获取sheet对象"""
        sheet = self.wk[sheet_name]return sheet

    defget_row_num(self, sheet):"""获取有效数据的最大行号"""return sheet.max_row

    defget_cols_num(self, sheet):"""获取有效数据的最大列号"""return  sheet.max_column

    defget_row_values(self, sheet, row_num):"""获取某一行数据"""
        max_cols_num = self.get_cols_num(sheet)
        row_values =[]for colsNum inrange(1, max_cols_num +1):
            value = sheet.cell(row_num, colsNum).value
            if value isNone:
                value =''
            row_values.append(value)returntuple(row_values)defget_column_values(self, sheet, column_num):"""获取某一列数据"""
        max_row_num = self.get_row_num(sheet)
        column_values =[]for rowNum inrange(2, max_row_num +1):
            value = sheet.cell (rowNum, column_num).value
            if value isNone:
                value =''
            column_values.append(value)returntuple(column_values)defget_value_of_cell(self, sheet, row_num, column_num):"""获取某一个单元格的数据"""
        value = sheet.cell(row_num, column_num).value
        if value isNone:
            value =''return value

    defget_all_values_of_sheet(self, sheet):"""获取某一个sheet页的所有测试数据,返回一个元祖组成的列表"""
        max_row_num = self.get_row_num(sheet)
        column_num = self.get_cols_num(sheet)
        all_values =[]for row inrange(2, max_row_num +1):
            row_values =[]for column inrange(1, column_num +1):
                value = sheet.cell(row, column).value
                if value isNone:
                    value =''
                row_values.append(value)
            all_values.append(tuple(row_values))return all_values

if __name__ =='__main__':pass
新建config.ini文件

config.ini文件是用于存放页面操作的UI元素的。

[LoginAccount];正确的登录账号和密码
# 网盘地址
NetDisk_IP = https://192.168.0.***
username=admin
password=***
user1=test1
password1=***[LoginPageElements];登录页面的元素
#用户名
username=xpath->//div[@id='login-targetEl']/div[2]/div/div/div/div/div[1]/div/div[1]/div/input#密码
password=xpath->//div[@id='login-targetEl']/div[2]/div/div/div/div/div[2]/div/div[1]/div/input#登录按钮
loginBtn=xpath->//div[@id='login-targetEl']/div[2]/div/div/div/div/a
#登录失败提示信息
errorText=xpath->//*[@id="messagebox-1001-msg"]#用户名为空提示信息
username_none=xpath->//span[(text()="Input error. 用户名不允许为空.")]#密码为空提示信息
passwd_none=xpath->//span[(text()="Input error. 密码不允许为空.")][HomePageElements];首页菜单栏元素
fileText=xpath->/html/body/div[1]/div/div/div[2]/div[1]/div/div/a[1]/span/span/span[2];注销登录页面元素

#注销下拉框按钮
logoutdrop_btn=xpath->/html/body/div[1]/div/div/div[3]/div/div/a[2]/span/span/span[2]#注销按钮
logout_btn=xpath->//*[@id="menuitem-1013-itemEl"][Create_folderPageElements];创建文件夹页面元素
# 创建文件夹按钮
create_folder_btn=xpath->//span[(text()="创建文件夹")]# 文件夹名称编辑框
folder_name_input=xpath->//*[@placeholder="请输入文件夹名称"]# 提交按钮
submit_btn=xpath->//span[(text()="提交")]# 取消按钮
cancel_btn=xpath->//span[(text()="取消")]# 关闭创建弹窗按钮
close_alert_btn=xpath->//*[@id="tool-1210"]# 创建成功提示信息
create_success_msg=xpath->//div[(text()="创建成功")]# 创建失败提示信息
create_failure_msg=xpath->//*[@id="messagebox-1001-msg"]# 提示信息确定按钮
msg_submit_btn=xpath->//*[@id="button-1005-btnInnerEl"]# 提示信息关闭按钮
msg_close_btn=xpath->//*[@id="tool-1219"]

新建data.py文件,用json格式编写测试数据,驱动测试用例执行不同的数据进行测试。

Login_data.py(用户登录功能的测试用例数据)
classLoginData(object):"""用户登录测试数据"""

login_success_data =[{"case":"用户名正确, 密码正确","username":"admin","password":"***","expected":"文件"}]#   登录失败
    login_fail_data =[{"case":"用户名正确, 密码错误","username":"admin","password":"admin","expected":"登录失败次数已超过设置次数,账户已锁定"},{"case":"用户名错误, 密码正确","username":"admin111","password":"***","expected":"账号或密码不正确"},{"case":"用户名错误, 密码错误","username":"admin123","password":"admin","expected":"账号或密码不正确"}]#   用户名、密码都为空
    login_user_pwd_none_data =[{"case":"用户名为空, 密码为空","username":"","password":"","expected1":"Input error. 用户名不允许为空.","expected2":"Input error. 密码不允许为空."}]if __name__ =='__main__':pass
Create_folder_data.py(创建文件夹功能的测试用例数据)
classCreateFolderData(object):"""新建文件夹测试数据"""#   文件夹名称输入中文测试
    folder_name_input_zh_data =[{"case":"文件夹名称输入中文","folder_name":"管理员","expected":"创建成功"}]#   输入大写英文
    folder_name_input_EN_data =[{"case":"文件夹名输入大写英文","folder_name":"ADMIN","expected":"创建成功"}]#   输入小写英文
    folder_name_input_en_data =[{"case":"文件夹名输入小写英文","folder_name":"admin","expected":"创建成功"}]#   文件夹名是否区分英文大小写
    folder_name_are_case_sensitive_data =[{"case":"文件夹名是否区分英文大小写","folder_name":"Admin","expected":"创建成功"}]#   文件夹名输入数字
    folder_name_input_num_data =[{"case":"文件夹名输入数字","folder_name":"123","expected":"创建成功"}]if __name__ =='__main__':pass

数据,UI对象库,解析方法都已经有了,接下来通过PageObject模式设计编写每个页面的操作及封装网盘的功能,以便后续设计用例调用

BasePage.py(webdriver等方法的二次封装)
# coding=utf-8import time
import os
import config.conf
import pywinauto
from pywinauto.keyboard import send_keys
from util.mylog import MyLog
from time import sleep
from selenium.webdriver import ActionChains
from selenium.webdriver.support.wait import WebDriverWait as WD
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.common.by import By
from selenium.common.exceptions import(
    TimeoutException,
    NoAlertPresentException,)from util.clipboard import ClipBoard
from util.keyboard import KeyBoard
from util.parseConFile import ParseConFile
from util.parseExcelFile import ParseExcel

classBasePage(object):"""结合显示等待封装一些selenium内置方法"""
    cf = ParseConFile()
    excel = ParseExcel()def__init__(self, driver, timeout=10):
        self.byDic ={'id': By.ID,'name': By.NAME,'class_name': By.CLASS_NAME,'xpath': By.XPATH,'link_text': By.LINK_TEXT
        }
        self.driver = driver
        self.outTime = timeout
        self.logger = MyLog().getLog()# 查找元素deffind_element(self, by, locator, model=None):"""
        find alone element
        :param by: eg: id, name, xpath, css.....
        :param locator: id, name, xpath for str
        :return: element object
        """try:
            element = WD(self.driver, self.outTime).until(lambda x: x.find_element(by, locator))except TimeoutException as t:print('error: found "{}" timeout!'.format(locator), t)# 截图
            self.save_webImgs(f"查找元素[{model}]异常")else:return element

    # 查找元素集deffind_elements(self, by, locator, model=None):"""
        find group elements
        :param by: eg: id, name, xpath, css.....
        :param locator: eg: id, name, xpath for str
        :return: elements object
        """
        self.logger.info(f'查找"{model}"元素集,元素定位:{locator}')try:
            elements = WD(self.driver, self.outTime).until(lambda x: x.find_elements(by, locator))except TimeoutException:
            self.logger.exception(f'查找"{model}"元素集失败,定位方式:{locator}')# 截图
            self.save_webImgs(f"查找元素集[{model}]异常")else:return elements

    # 断言元素是否存在defis_element_exist(self, by, locator, model=None):"""
        assert element if exist
        :param by: eg: id, name, xpath, css.....
        :param locator: eg: id, name, xpath for str
        :return: if element return True else return false
        """
        self.logger.info(f'断言"{model}"元素存在,元素定位:{locator}')if by.lower()in self.byDic:try:
                WD(self.driver, self.outTime). \
                    until(ec.visibility_of_element_located((self.byDic[by], locator)))except TimeoutException:
                self.logger.exception(f'断言"{model}"元素不存在,定位方式:{locator}')# 截图
                self.save_webImgs(f"断言元素[{model}]异常")returnFalsereturnTrueelse:print('the "{}" error!'.format(by))# 点击操作defis_click(self, by, locator, model=None):if by.lower()in self.byDic:try:
                element = WD(self.driver, self.outTime). \
                    until(ec.element_to_be_clickable((self.byDic[by], locator)))except TimeoutException:# 截图
                self.save_webImgs(f"[{model}]点击异常")else:return element
        else:print('the "{}" error!'.format(by))defis_alert(self):"""
        assert alert if exsit
        :return: alert obj
        """try:
            re = WD(self.driver, self.outTime).until(ec.alert_is_present())except(TimeoutException, NoAlertPresentException):print("error:no found alert")else:return re

    #  切换 iframedefswitch_to_frame(self, by, locator):"""判断frame是否存在,存在就跳到frame"""# print('info:switching to iframe "{}"'.format(locator))
        self.logger.info('iframe 切换操作:')if by.lower()in self.byDic:try:
                WD(self.driver, self.outTime). \
                    until(ec.frame_to_be_available_and_switch_to_it((self.byDic[by], locator)))
                sleep(0.5)
                self.logger.info('切换成功')except TimeoutException:
                self.logger.exception('iframe 切换失败!!!')# 截图
                self.save_webImgs(f"iframe切换异常")else:print('the "{}" error!'.format(by))# 返回默认iframedefswitch_to_default_frame(self):"""返回默认的frame"""# print('info:switch back to default iframe')
        self.logger.info('切换到默认页面')try:
            self.driver.switch_to.default_content()
            self.logger.info('返回默认frame成功')except:
            self.logger.exception('返回默认窗口失败!!!')# 截图
            self.save_webImgs("切换失败_没有要切换窗口的信息")raisedefget_alert_text(self):"""获取alert的提示信息"""
        alert = self.is_alert()if alert:return alert.text
        else:returnNonedefswitch_to_alert_accept(self):"""处理浏览器弹窗信息,点击确定"""
        self.logger.info('处理浏览器弹窗信息')try:
            self.driver.switch_to.alert.accept()except:
            self.logger.exception(f'浏览器弹窗信息处理失败!')# 截图
            self.save_webImgs(f"浏览器弹窗信息处理失败!")raisedefswitch_to_alert_dismiss(self):"""处理浏览器弹窗信息,点击取消"""
        self.logger.info('处理浏览器弹窗信息')try:
            alert = self.driver.switch_to.alert.dismiss()
            alert.dismiss()except AttributeError:
            self.logger.exception(f'浏览器弹窗信息处理失败!')# 截图
            self.save_webImgs(f"浏览器弹窗信息处理失败!")raise# 获取某一个元素的text信息defget_element_text(self, by, locator, name=None, model=None):"""获取某一个元素的text信息"""try:
            element = self.find_element(by, locator)if name:return element.get_attribute(name)else:
                self.logger.info(f'获取"{model}"元素文本内容为"{element.text}",元素定位:{locator}')return element.text
        except AttributeError:
            self.logger.exception(f'获取"{model}"元素文本内容失败,元素定位:{locator}')# 截图
            self.save_webImgs(f"获取[{model}]文本内容异常")# 获取多个元素的text信息defget_elements_text(self, by, locator, model=None):"""获取多个元素的text信息"""try:
            element = self.find_elements(by, locator)
            text_list =[]for i in element:
                text = i.text
                text_list.append(text)return text_list
        except AttributeError:
            self.logger.exception(f'获取多个"{model}"元素文本内容失败,元素定位:{locator}')# 截图
            self.save_webImgs(f"获取多个[{model}]文本内容异常")# print('get "{}" get_attribute failed return None'.format(locator))returnNone# 加载urldefload_url(self, url):"""加载url"""# print('info: string upload url "{}"'.format(url))
        self.driver.get(url)# 获取页面源码defget_source(self):"""获取页面源码"""return self.driver.page_source

    # 写数据defsend_keys(self, by, locator, value='', model=None):"""写数据"""# print('info:input "{}"'.format(value))
        self.logger.info(f'在"{model}"输入"{value}",元素定位:{locator}')try:
            element = self.find_element(by, locator)
            element.send_keys(value)except AttributeError:
            self.logger.exception(f'"{model}"输入操作失败!')# 截图
            self.save_webImgs(f"[{model}]输入异常")# 清理数据defclear(self, by, locator, model=None):"""清理数据"""
        self.logger.info(f'清除"{model}",元素定位:{locator}')try:
            element = self.find_element(by, locator)
            element.clear()except AttributeError:
            self.logger.exception(f'"{model}"清除操作失败')# 截图
            self.save_webImgs(f"[{model}]清除异常")# 点击某个元素defclick(self, by, locator, model=None):"""点击某个元素"""
        element = self.is_click(by, locator)
        self.logger.info(f'点击"{model}",元素定位:{locator}')if element:
            element.click()else:
            self.logger.exception(f'"{model}"点击失败')# 截图
            self.save_webImgs(f"[{model}]点击异常")# print('the "{}" unclickable!')# 双击元素defdouble_click(self, by, locator, model=None):"""点击某个元素两次"""# print('info:double_click "{}"'.format(locator))
        element = self.is_click(by, locator)
        xpath = self.find_element(by, locator)
        self.logger.info(f'双击"{model}",元素定位:{locator}')if element:
            ActionChains(self.driver).double_click(xpath).perform()else:
            self.logger.exception(f'"{model}"双击失败')# 截图
            self.save_webImgs(f"[{model}]双击异常")# print('the "{}" unclickable!')@staticmethoddefsleep(num=0):"""强制等待"""# print('info:sleep "{}" minutes'.format(num))
        time.sleep(num)defctrl_v(self, value):"""ctrl + V 粘贴"""# print('info:pasting "{}"'.format(value))
        ClipBoard.setText(value)
        self.sleep(2)
        KeyBoard.twoKeys('ctrl','v')@staticmethoddefenter_key():"""enter 回车键"""# print('info:keydown enter')
        KeyBoard.oneKey('enter')defsend_emoji(self, by, locator, value=''):# print('info:input "{}"'.format(value))
        js_add_text_to_input ="""
          var elm = arguments[0], txt = arguments[1];
          elm.value += txt;
          elm.dispatchEvent(new Event('change'));
          """try:
            element = self.find_element(by, locator)
            self.driver.execute_script(js_add_text_to_input, element, value)except AttributeError as e:print(e)# 等待元素可见defwait_element_to_be_located(self, by, locator, model=None):"""显示等待某个元素出现,且可见"""
        self.logger.info(f'等待"{model}"元素,定位方式:{locator}')try:return WD(self.driver, self.outTime).until(ec.presence_of_element_located((self.byDic[by], locator)))except TimeoutException:
            self.logger.exception(f'等待"{model}"元素失败,定位方式:{locator}')# 截图
            self.save_webImgs(f"等待元素[{model}]出现异常")defget_page_source(self):return self.get_source()defsave_webImgs(self, model=None):# filepath = 指图片保存目录/model(页面功能名称)_当前时间到秒.png# 截图保存目录# 拼接截图文件夹,如果不存在则自动创建
        cur_path = config.conf.cur_path
        now_date = config.conf.CURRENT_TIME
        screenshots_path = os.path.join(os.path.dirname(cur_path),f'Screenshots\\{now_date}')ifnot os.path.exists(screenshots_path):
            os.makedirs(screenshots_path)# 当前时间
        datenow = time.strftime('%Y%m%d_%H%M%S', time.localtime(time.time()))# 路径
        filepath ='{}\\{}_{}.png'.format(screenshots_path, model, datenow)try:
            self.driver.save_screenshot(filepath)
            self.logger.info(f"截屏成功,图片路径为{filepath}")except:
            self.logger.exception('截屏失败!')raisedefupload_file(self, filePath, filename):"""上传文件"""try:
            self.logger.info("上传文件")# 使用pywinauto来选择文件
            app = pywinauto.Desktop()# 选择文件上传的窗口
            dlg = app["打开"]# 选择文件地址输入框
            dlg["Toolbar3"].click()# 键盘输入上传文件的路径
            send_keys(filePath)# 键盘输入回车,打开该路径
            send_keys("{VK_RETURN}")# 选中文件名输入框,输入文件名
            dlg["文件名(&N):Edit"].type_keys(filename)# 点击打开
            dlg["打开(&O)"].click()except:
            self.logger.info("上传失败!")raisedefupload_folder(self, filePath, filename):"""上传文件夹"""try:
            self.logger.info("上传文件夹")# 使用pywinauto来选择文件夹
            app = pywinauto.Desktop()# 选择文件夹上传的窗口
            dlg = app["选择要上传的文件夹"]# 选择文件地址输入框
            dlg["Toolbar3"].click()# 键盘输入上传文件的路径
            send_keys(filePath)# 键盘输入回车,打开该路径
            send_keys("{VK_RETURN}")# 选中文件名输入框,输入文件名
            dlg["文件名(&N):Edit"].type_keys(filename)# 点击打开
            dlg["上传"].click()except:
            self.logger.info("上传文件夹失败!")raiseif __name__ =="__main__":pass
LoginPage.py(封装登录功能)
from Page.BasePage import BasePage
from util.parseConFile import ParseConFile
from util.keyboard import KeyBoard

# 网盘IP地址
Netdisk_ip = ParseConFile().get_locators_or_account('LoginAccount','NetDisk_IP')classLoginPage(BasePage):# 配置文件读取元素
    do_conf = ParseConFile()# 用户名输入框
    username = do_conf.get_locators_or_account('LoginPageElements','username')# 密码输入框
    password = do_conf.get_locators_or_account('LoginPageElements','password')# 登录按钮
    loginBtn = do_conf.get_locators_or_account('LoginPageElements','loginBtn')# 登录失败的提示信息
    error_Text = do_conf.get_locators_or_account('LoginPageElements','errorText')# 登录成功后的显示信息
    fileText = do_conf.get_locators_or_account('HomePageElements','fileText')deflogin(self, username, password):"""登录流程"""
        self.logger.info("【===登录操作===】")
        self.open_url()
        self.input_username(username)
        self.input_password(password)
        self.click_login_btn()defopen_url(self):"""加载URL"""
        self.logger.info("【===打开URL===】")return self.load_url(url=Netdisk_ip)defclick_username(self):"""点击用户名编辑框"""
        self.wait_element_to_be_located(*LoginPage.username, model='用户名框')return self.click(*LoginPage.username, model='用户名框')definput_username(self, username):"""输入用户名"""
        self.wait_element_to_be_located(*LoginPage.username, model='用户名框')return self.send_keys(*LoginPage.username, username, model='用户名框')defclick_password(self):"""点击密码编辑框"""
        self.wait_element_to_be_located(*LoginPage.password, model='密码框')return self.click(*LoginPage.password, model='密码框')definput_password(self, password):"""输入密码"""
        self.wait_element_to_be_located(*LoginPage.password, model='密码框')return self.send_keys(*LoginPage.password, password, model='密码框')defclick_login_btn(self):"""点击登录按钮"""
        self.wait_element_to_be_located(*LoginPage.loginBtn, model='登录按钮')return self.click(*LoginPage.loginBtn, model='登录按钮')defget_error_text(self):"""用户登录失败提示信息"""
        self.logger.info("【===获取登录失败报错信息===】")return self.get_element_text(*LoginPage.error_Text, model='登录失败的验证信息')defget_login_success_text(self):"""用户登录成功验证信息"""
        self.logger.info("【===获取登录成功验证信息===】")return self.get_element_text(*LoginPage.fileText, model="登录成功的验证信息")defget_username_none_text(self):"""登录用户名为空时,提示信息"""
        self.logger.info("【===获取用户名为空时,提示信息===】")return self.get_element_text(*LoginPage.username_none, model='用户名为空时,提示信息')defget_passwd_none_text(self):"""登录密码为空时,提示信息"""
        self.logger.info("【===获取密码为空时,提示信息===】")return self.get_element_text(*LoginPage.password_none, model='密码为空时,提示信息')
CreatefolderPage.py(封装创建文件夹功能)
from Page.BasePage import BasePage
from util.parseConFile import ParseConFile
from util.keyboard import KeyBoard

classCreateFolderPage(BasePage):# 配置文件读取元素
    do_conf = ParseConFile()# 创建文件夹按钮
    create_folder_btn = do_conf.get_locators_or_account('Create_folderPageElements','create_folder_btn')# 文件夹名称编辑框
    folder_name_input = do_conf.get_locators_or_account('Create_folderPageElements','folder_name_input')# 提交按钮
    submit_btn = do_conf.get_locators_or_account('Create_folderPageElements','submit_btn')# 取消按钮
    cancel_btn = do_conf.get_locators_or_account('Create_folderPageElements','cancel_btn')# 关闭创建弹窗按钮
    close_alert_btn = do_conf.get_locators_or_account('Create_folderPageElements','close_alert_btn')# 创建成功提示信息
    create_success_msg = do_conf.get_locators_or_account('Create_folderPageElements','create_success_msg')# 创建失败提示信息
    create_failure_msg = do_conf.get_locators_or_account('Create_folderPageElements','create_failure_msg')defcreate_folder(self, folder_name):"""创建文件夹操作流程"""
        self.logger.info('【===创建文件夹操作流程===】')
        self.click_create_folder_btn()
        self.input_folder_name(folder_name)
        self.click_submit_btn()defclick_create_folder_btn(self):"""点击创建文件夹按钮"""
        self.wait_element_to_be_located(*CreateFolderPage.create_folder_btn, model='新建文件夹按钮')return self.click(*CreateFolderPage.create_folder_btn, model='新建文件夹按钮')definput_folder_name(self, folder_name):"""输入文件夹名称"""
        self.wait_element_to_be_located(*CreateFolderPage.folder_name_input, model='文件夹名称编辑框')return self.send_keys(*CreateFolderPage.folder_name_input, folder_name, model='文件夹名称编辑框')defclick_submit_btn(self):"""点击提交按钮"""
        self.wait_element_to_be_located(*CreateFolderPage.submit_btn, model='提交按钮')return self.click(*CreateFolderPage.submit_btn, model='提交按钮')defclick_cancel_btn(self):"""点击取消按钮"""
        self.wait_element_to_be_located(*CreateFolderPage.cancel_btn, model='取消按钮')return self.click(*CreateFolderPage.cancel_btn, model='取消按钮')defclick_close_btn(self):"""点击关闭按钮"""
        self.wait_element_to_be_located(*CreateFolderPage.close_alert_btn, model='关闭按钮')return self.click(*CreateFolderPage.close_alert_btn, model='关闭按钮')defget_error_msg(self):"""获取创建文件夹失败提示信息"""
        self.wait_element_to_be_located(*CreateFolderPage.create_failure_msg, model='创建文件夹失败提示信息')return self.get_element_text(*CreateFolderPage.create_failure_msg, model='创建文件夹失败提示信息')defclick_close_error_msg(self):"""点击关闭错误提示信息按钮"""
        self.wait_element_to_be_located(*CreateFolderPage.msg_close_btn, model='关闭错误提示信息按钮')return self.click(*CreateFolderPage.msg_close_btn, model='关闭错误提示信息按钮')defclick_submit_error_msg(self):"""点击错误信息提示框确定按钮"""
        self.wait_element_to_be_located(*CreateFolderPage.msg_submit_btn, model='确定按钮')return self.click(*CreateFolderPage.msg_submit_btn, model='确定按钮')defget_success_msg(self):"""创建成功提示信息"""
        self.wait_element_to_be_located(*CreateFolderPage.create_success_msg, model='创建成功信息')return self.get_element_text(*CreateFolderPage.create_success_msg, model='创建成功信息')if __name__ =="__main__":pass

所有的准备工作都已经做好了,还有一个问题,我们的创建文件夹功能是否应该在已经登录的前提下测试呢?答案是肯定的。所以我们在用例同目录下新建conftest.py文件并调用登录功能(为什么这么做,不明白的小伙伴可以去搜一下关于conftest.py的原理,后面我也会单独写一篇文章讲一下这个原理。)

conftest.py(除登录用例外,其他用例运行的前置条件)
import pytest
from util.parseConFile import ParseConFile
from Page.PageObject.LoginPage import LoginPage
from Page.PageObject.CreatefolderPage import CreateFolderPage

do_conf = ParseConFile()# 从配置文件中获取正确的用户名和密码
userName = do_conf.get_locators_or_account('LoginAccount','username')
passWord = do_conf.get_locators_or_account('LoginAccount','password')@pytest.fixture(scope='class')defini_pages(driver):
    login_page = LoginPage(driver)
    create_folder_page = CreateFolderPage(driver)yield driver, login_page, create_folder_page

@pytest.fixture(scope='function')defopen_url(ini_pages):
    driver = ini_pages[0]
    login_page = ini_pages[1]# login_page.open_url()yield login_page
    driver.delete_all_cookies()@pytest.fixture(scope='class')deflogin(ini_pages):
    driver, login_page, create_folder_page = ini_pages
    login_page.login(userName, passWord)yield login_page, create_folder_page
    driver.delete_all_cookies()@pytest.fixture(scope='function')defrefresh_page(ini_pages):
    driver = ini_pages[0]yield
    driver.refresh()

到这里,准备工作就全部完成了,就可以写测试用例了。

test_001_loginCase.py(登录功能测试用例)
import allure
import pytest
from data.Login.login_data import LoginData

@allure.feature("登录功能")@allure.description("登录界面功能测试")classTestLogin(object):# 测试数据
    login_data = LoginData

    @[email protected]("正确的用户名密码测试")@allure.title("登录成功场景")@pytest.mark.parametrize("data", login_data.login_success_data)deftest_login(self, open_url, data):
        login_page = open_url
        login_page.login(data["username"], data["password"])
        result = login_page.get_login_success_text()assert result == data["expected"]@allure.story("登录失败测试")@allure.title("登录失败场景")@pytest.mark.parametrize('data', login_data.login_fail_data)deftest_fail(self, open_url, data):
        login_page = open_url
        login_page.login(data["username"], data["password"])
        actual = login_page.get_error_text()assert actual == data["expected"]@allure.story("用户名、密码都为空测试")@allure.title("用户名、密码均不输入")@pytest.mark.parametrize('data', login_data.login_user_pwd_none_data)deftest_login_none(self, open_url, data):
        login_page = open_url
        login_page.login_none(data["username"], data["password"])
        actual1 = login_page.get_username_none_text()
        actual2 = login_page.get_passwd_none_text()assert actual1 == data["expected1"]assert actual2 == data["expected2"]@allure.story("登录界面编辑框复制粘贴测试")@allure.title("登录界面编辑框复制粘贴场景")@pytest.mark.parametrize("data", login_data.login_input_copy_data)deftest_login_input_copy(self, open_url, data):
        login_page = open_url
        login_page.login_input_copy(data["username"], data["password"])
        result = login_page.get_error_text()assert result == data["expected"]@allure.story("登录界面编辑框剪切粘贴测试")@allure.title("登录界面编辑框剪切粘贴场景")@pytest.mark.parametrize("data", login_data.login_input_copy_data)deftest_login_input_cut(self, open_url, data):
        login_page = open_url
        login_page.login_input_copy(data["username"], data["password"])
        result = login_page.get_error_text()assert result == data["expected"]if __name__ =="__main__":
    pytest.main()
test_002_createfolderCase.py(新建文件夹测试用例)
import allure
import pytest
from data.Create_folder.create_folder_data import CreateFolderData

@allure.feature("新建文件夹功能")@allure.description("新建文件夹功能测试")classTestCreateFolder(object):# 测试数据
    create_folder_data = CreateFolderData

    @[email protected]("新建文件夹名称编辑框测试")@allure.title("输入中文测试")@pytest.mark.parametrize("data", create_folder_data.folder_name_input_zh_data)deftest_create_folder_name_input_zh(self, login, data):
        create_page = login[2]
        create_page.create_folder(data["folder_name"])
        result = create_page.get_success_msg()assert result == data["expected"]@allure.story("新建文件夹名称编辑框测试")@allure.title("输入大写英文测试")@pytest.mark.parametrize("data", create_folder_data.folder_name_input_EN_data)deftest_create_folder_name_input_eng(self, login, data):
        create_page = login[2]
        create_page.create_folder(data["folder_name"])
        result = create_page.get_success_msg()assert result == data["expected"]@allure.story("新建文件夹名称编辑框测试")@allure.title("输入小写英文测试")@pytest.mark.parametrize("data", create_folder_data.folder_name_input_EN_data)deftest_create_folder_name_input_en(self, login, data):
        create_page = login[2]
        create_page.create_folder(data["folder_name"])
        result = create_page.get_success_msg()assert result == data["expected"]@allure.story("新建文件夹名称编辑框测试")@allure.title("文件夹名是否区分英文大小写")@pytest.mark.parametrize("data", create_folder_data.folder_name_are_case_sensitive_data)deftest_create_folder_name_input_sen(self, login, data):
        create_page = login[2]
        create_page.create_folder(data["folder_name"])
        result = create_page.get_success_msg()assert result == data["expected"]@[email protected]("新建文件夹名称编辑框测试")@allure.title("文件夹名输入数字")@pytest.mark.parametrize("data", create_folder_data.folder_name_input_num_data)deftest_create_folder_name_input_num(self, login, data):
        create_page = login[2]
        create_page.create_folder(data["folder_name"])
        result = create_page.get_success_msg()assert result == data["expected"]if __name__ =="__main__":
    pytest.main()

问题

用例已经写完了,有两个问题

1.有没有发现我们的报告怎么生成的?也没有失败用例截图?

2.我们貌似并没有编写驱动浏览器的代码?

现在我们来解决这个两个问题。

根据pytest的conftest.py文件的原理,我们可以把驱动浏览器的代码写在一个全局的conftest.py文件里面。

conftest.py(全局conftest.py文件)
import allure
import pytest
from selenium import webdriver

driver =None# 测试失败时添加截图defallure_screenshot():# 添加allure失败截图
    allure.attach(driver.get_screenshot_as_png(),"失败截图", allure.attachment_type.PNG)# 设置为session,全部用例执行一次@pytest.fixture(scope='session')defdriver():global driver
    print('------------open browser------------')
    chromeOptions = webdriver.ChromeOptions()# 设定下载文件的保存目录,# 如果该目录不存在,将会自动创建
    prefs ={"download.default_directory":"E:\\testDownload"}# 将自定义设置添加到Chrome配置对象实例中
    chromeOptions.add_experimental_option("prefs", prefs)
    chromeOptions.add_argument("--ignore-certificate-errors")# chromeOptions.add_argument('--disable-gpu')
    chromeOptions.add_argument('--unlimited-storage')
    driver = webdriver.Chrome(options=chromeOptions)# driver = webdriver.Chrome()
    driver.maximize_window()# driver.implicitly_wait(10)yield driver
    print('------------close browser------------')
    driver.quit()
allure 生成自动化测试结果

安装:pip install allure-pytest

1.配置
只需要在测试用例的方法前: @allure.story(‘示例’)
需要查看步骤的方法前:@allure.step(‘示例步骤’)

2.生成结果

pytest ./TestCases/test_001_loginCase.py  --alluredir ./Report

执行testcases目录下的test_001_loginCase.py文件中的所有用例,生成结果到根目录下的Report目录下

3.生成报告

allure generate Report -o Report/html --clean

将report 目录下的测试结果整理,生成到html文件夹下。其中index.html 使用浏览器打开后查看结果。

是不是发现这样输命令这种方式感觉有点low,我们换另外一种方式,可以通过os模块自动执行相关命令,编写运行用例代码,一键运行生成测试报告。

Run_TestCase.py
import os
import pytest
import config.conf

if __name__ =='__main__':# 当前时间
    now_time = config.conf.CURRENT_TIME
    # allure 测试报告路径
    cur_path = config.conf.cur_path
    report_path = os.path.join(cur_path,f'..\\Report\\{now_time}')# -s : 打印信息# -m :运行含标签的用例# 指定某一模块的测试用例执行,例如:'./TestCases/test_003_createfolderCase.py'
    pytest.main(['./TestCases/test_002_uploadCase.py',"--alluredir", report_path])# 解析测试报告,执行: allure serve {report_path}
    os.system(f"allure serve {report_path}")

最后

为了减小项目维护成本,我们把一些全局的配置项,放到我们的功能配置文件中共全局使用,包括运行用例的一些命令字符串,可以自行修改

conf.py(全局配置文件)
import time
import os

# 项目根目录
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))# ui元素对象库config.ini文件所在目录
CONF_PATH = os.path.join(ROOT_DIR,'config','config.ini')# 测试数据所在目录
DATA_Path = os.path.join(ROOT_DIR,'data','tcData.xlsx')# 上传文件数据所在目录
upload_data = os.path.join(ROOT_DIR,'data',f'Upload\\files')# 上传文件夹数据所在目录
upload_dir = os.path.join(ROOT_DIR,'data',f'Upload\\folder')# 测试用例所在目录
cases_dir = os.path.join(ROOT_DIR,'TestCases')# 当前时间
CURRENT_TIME = time.strftime('%Y%m%d-%H%M%S', time.localtime(time.time()))# 报告目录
cur_path = os.path.dirname(os.path.realpath(__file__))
report_path = os.path.join(cur_path,f'Report\\{CURRENT_TIME}')

测试输出

1.自动生成allure测试报告,其中报告里面附带用例执行日志明细,及用例失败自动截图(部分报告展示)
在这里插入图片描述
在这里插入图片描述

项目源码

源码在我的Gitee上,想要源码的话,进QQ群看公告就可以啦。

总而言之

自动化测试是一个不断学习不断更新的东西,我们都需要时刻准备接受新的知识!

PS:最后还是附上我们的[QQ交流群]:516846105 真心希望所有对测试感兴趣,想入门,想提升自己测试能力的小伙伴加入!

标签: python selenium pytest

本文转载自: https://blog.csdn.net/qq350146607/article/details/128264156
版权归原作者 高级拳师 所有, 如有侵权,请联系我们删除。

“Python+Selenium+Pytest+Allure自动化测试框架实战实例(示例为我司网盘产品)”的评论:

还没有评论