前言
应公司要求,组织员工培训自动化测试,所以也趁此机会把我所学习的自动化框架整理一下,虽说不是很完美,但也有所收获。
环境准备
序号 | 库、插件、工具 | 版本号 |
---|---|---|
1 | Python | 3.11 |
2 | Pycharm | 22.2.3 |
3 | pytest | 7.2.0 |
4 | pywin32 | 305 |
5 | selenium3 | 4.6.0 |
6 | openpyxl | 3.0.10 |
7 | Chromedriver | 与当前浏览器版本对应即可 |
8 | allure | 2.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
class ClipBoard(object):
'''设置剪切板内容和获取剪切板内容'''
@staticmethod
def getText():
'''获取剪切板内容'''
WC.OpenClipboard()
value = WC.GetClipboardData(win32con.CF_TEXT)
WC.CloseClipboard()
return value
@staticmethod
def setText(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
class KeyBoard(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,
}
@staticmethod
def multiple_go_down(num, name):
# 多次按下同一个键
t = 1
while True:
if t <= num:
KeyBoard().keyDown(name)
t += 1
time.sleep(0.5)
else:
break
@staticmethod
def keyDown(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)
@staticmethod
def keyUp(key_name):
"""抬起键"""
key_name = key_name.lower()
win32api.keybd_event(KeyBoard.vk_code[key_name], 0, win32con.KEYEVENTF_KEYUP, 0)
@staticmethod
def oneKey(key):
"""模拟单个键"""
key = key.lower()
KeyBoard.keyDown(key)
time.sleep(0.1)
KeyBoard.keyUp(key)
@staticmethod
def twoKeys(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
class MyLog(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')
if not 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}')
if not 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)
def getLog(self):
return self.logger
通过测试项目设计,我们需要把测试数据存放在Excel文件中,把页面操作元素存在UI对象库中也就是一个配置文件,那么我们需要对Excel 和 ini文件解析,因此我们开始编写这两个方法,设计UI对象库和测试数据文件。
parseConfile.py(读取配置文件的方法)
import configparser
from config.conf import CONF_PATH
class ParseConFile(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)
def get_all_sections(self):
"""获取所有的section,返回一个列表"""
return self.conf.sections()
def get_all_options(self, section):
"""获取指定section下所有的option, 返回列表"""
return self.conf.options(section)
def get_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)
def get_option_value(self, section):
"""获取指定section下所有的option和对应的数据,返回字典"""
value = dict(self.conf.items(section))
return value
def get_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
class ParseExcel(object):
def __init__(self):
self.wk = load_workbook(DATA_Path)
self.excelFile = DATA_Path
def get_sheet_by_name(self, sheet_name):
"""获取sheet对象"""
sheet = self.wk[sheet_name]
return sheet
def get_row_num(self, sheet):
"""获取有效数据的最大行号"""
return sheet.max_row
def get_cols_num(self, sheet):
"""获取有效数据的最大列号"""
return sheet.max_column
def get_row_values(self, sheet, row_num):
"""获取某一行数据"""
max_cols_num = self.get_cols_num(sheet)
row_values = []
for colsNum in range (1, max_cols_num + 1):
value = sheet.cell(row_num, colsNum).value
if value is None:
value = ''
row_values.append(value)
return tuple(row_values)
def get_column_values(self, sheet, column_num):
"""获取某一列数据"""
max_row_num = self.get_row_num(sheet)
column_values = []
for rowNum in range (2, max_row_num + 1):
value = sheet.cell (rowNum, column_num).value
if value is None:
value = ''
column_values.append(value)
return tuple(column_values)
def get_value_of_cell(self, sheet, row_num, column_num):
"""获取某一个单元格的数据"""
value = sheet.cell(row_num, column_num).value
if value is None:
value = ''
return value
def get_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 in range(2, max_row_num + 1):
row_values = []
for column in range(1, column_num + 1):
value = sheet.cell(row, column).value
if value is None:
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(用户登录功能的测试用例数据)
class LoginData(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(创建文件夹功能的测试用例数据)
class CreateFolderData(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-8
import 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
class BasePage(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()
# 查找元素
def find_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
# 查找元素集
def find_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
# 断言元素是否存在
def is_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}]异常")
return False
return True
else:
print('the "{}" error!'.format(by))
# 点击操作
def is_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))
def is_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
# 切换 iframe
def switch_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))
# 返回默认iframe
def switch_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("切换失败_没有要切换窗口的信息")
raise
def get_alert_text(self):
"""获取alert的提示信息"""
alert = self.is_alert()
if alert:
return alert.text
else:
return None
def switch_to_alert_accept(self):
"""处理浏览器弹窗信息,点击确定"""
self.logger.info('处理浏览器弹窗信息')
try:
self.driver.switch_to.alert.accept()
except:
self.logger.exception(f'浏览器弹窗信息处理失败!')
# 截图
self.save_webImgs(f"浏览器弹窗信息处理失败!")
raise
def switch_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信息
def get_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信息
def get_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))
return None
# 加载url
def load_url(self, url):
"""加载url"""
# print('info: string upload url "{}"'.format(url))
self.driver.get(url)
# 获取页面源码
def get_source(self):
"""获取页面源码"""
return self.driver.page_source
# 写数据
def send_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}]输入异常")
# 清理数据
def clear(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}]清除异常")
# 点击某个元素
def click(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!')
# 双击元素
def double_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!')
@staticmethod
def sleep(num=0):
"""强制等待"""
# print('info:sleep "{}" minutes'.format(num))
time.sleep(num)
def ctrl_v(self, value):
"""ctrl + V 粘贴"""
# print('info:pasting "{}"'.format(value))
ClipBoard.setText(value)
self.sleep(2)
KeyBoard.twoKeys('ctrl', 'v')
@staticmethod
def enter_key():
"""enter 回车键"""
# print('info:keydown enter')
KeyBoard.oneKey('enter')
def send_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)
# 等待元素可见
def wait_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}]出现异常")
def get_page_source(self):
return self.get_source()
def save_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}')
if not 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('截屏失败!')
raise
def upload_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("上传失败!")
raise
def upload_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("上传文件夹失败!")
raise
if __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')
class LoginPage(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')
def login(self, username, password):
"""登录流程"""
self.logger.info("【===登录操作===】")
self.open_url()
self.input_username(username)
self.input_password(password)
self.click_login_btn()
def open_url(self):
"""加载URL"""
self.logger.info("【===打开URL===】")
return self.load_url(url=Netdisk_ip)
def click_username(self):
"""点击用户名编辑框"""
self.wait_element_to_be_located(*LoginPage.username, model='用户名框')
return self.click(*LoginPage.username, model='用户名框')
def input_username(self, username):
"""输入用户名"""
self.wait_element_to_be_located(*LoginPage.username, model='用户名框')
return self.send_keys(*LoginPage.username, username, model='用户名框')
def click_password(self):
"""点击密码编辑框"""
self.wait_element_to_be_located(*LoginPage.password, model='密码框')
return self.click(*LoginPage.password, model='密码框')
def input_password(self, password):
"""输入密码"""
self.wait_element_to_be_located(*LoginPage.password, model='密码框')
return self.send_keys(*LoginPage.password, password, model='密码框')
def click_login_btn(self):
"""点击登录按钮"""
self.wait_element_to_be_located(*LoginPage.loginBtn, model='登录按钮')
return self.click(*LoginPage.loginBtn, model='登录按钮')
def get_error_text(self):
"""用户登录失败提示信息"""
self.logger.info("【===获取登录失败报错信息===】")
return self.get_element_text(*LoginPage.error_Text, model='登录失败的验证信息')
def get_login_success_text(self):
"""用户登录成功验证信息"""
self.logger.info("【===获取登录成功验证信息===】")
return self.get_element_text(*LoginPage.fileText, model="登录成功的验证信息")
def get_username_none_text(self):
"""登录用户名为空时,提示信息"""
self.logger.info("【===获取用户名为空时,提示信息===】")
return self.get_element_text(*LoginPage.username_none, model='用户名为空时,提示信息')
def get_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
class CreateFolderPage(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')
def create_folder(self, folder_name):
"""创建文件夹操作流程"""
self.logger.info('【===创建文件夹操作流程===】')
self.click_create_folder_btn()
self.input_folder_name(folder_name)
self.click_submit_btn()
def click_create_folder_btn(self):
"""点击创建文件夹按钮"""
self.wait_element_to_be_located(*CreateFolderPage.create_folder_btn, model='新建文件夹按钮')
return self.click(*CreateFolderPage.create_folder_btn, model='新建文件夹按钮')
def input_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='文件夹名称编辑框')
def click_submit_btn(self):
"""点击提交按钮"""
self.wait_element_to_be_located(*CreateFolderPage.submit_btn, model='提交按钮')
return self.click(*CreateFolderPage.submit_btn, model='提交按钮')
def click_cancel_btn(self):
"""点击取消按钮"""
self.wait_element_to_be_located(*CreateFolderPage.cancel_btn, model='取消按钮')
return self.click(*CreateFolderPage.cancel_btn, model='取消按钮')
def click_close_btn(self):
"""点击关闭按钮"""
self.wait_element_to_be_located(*CreateFolderPage.close_alert_btn, model='关闭按钮')
return self.click(*CreateFolderPage.close_alert_btn, model='关闭按钮')
def get_error_msg(self):
"""获取创建文件夹失败提示信息"""
self.wait_element_to_be_located(*CreateFolderPage.create_failure_msg, model='创建文件夹失败提示信息')
return self.get_element_text(*CreateFolderPage.create_failure_msg, model='创建文件夹失败提示信息')
def click_close_error_msg(self):
"""点击关闭错误提示信息按钮"""
self.wait_element_to_be_located(*CreateFolderPage.msg_close_btn, model='关闭错误提示信息按钮')
return self.click(*CreateFolderPage.msg_close_btn, model='关闭错误提示信息按钮')
def click_submit_error_msg(self):
"""点击错误信息提示框确定按钮"""
self.wait_element_to_be_located(*CreateFolderPage.msg_submit_btn, model='确定按钮')
return self.click(*CreateFolderPage.msg_submit_btn, model='确定按钮')
def get_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')
def ini_pages(driver):
login_page = LoginPage(driver)
create_folder_page = CreateFolderPage(driver)
yield driver, login_page, create_folder_page
@pytest.fixture(scope='function')
def open_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')
def login(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')
def refresh_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("登录界面功能测试")
class TestLogin(object):
# 测试数据
login_data = LoginData
@pytest.mark.Login
@allure.story("正确的用户名密码测试")
@allure.title("登录成功场景")
@pytest.mark.parametrize("data", login_data.login_success_data)
def test_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)
def test_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)
def test_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)
def test_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)
def test_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("新建文件夹功能测试")
class TestCreateFolder(object):
# 测试数据
create_folder_data = CreateFolderData
@pytest.mark.CreateFolder
@allure.story("新建文件夹名称编辑框测试")
@allure.title("输入中文测试")
@pytest.mark.parametrize("data", create_folder_data.folder_name_input_zh_data)
def test_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)
def test_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)
def test_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)
def test_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"]
@pytest.mark.CreateFolder
@allure.story("新建文件夹名称编辑框测试")
@allure.title("文件夹名输入数字")
@pytest.mark.parametrize("data", create_folder_data.folder_name_input_num_data)
def test_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
# 测试失败时添加截图
def allure_screenshot():
# 添加allure失败截图
allure.attach(driver.get_screenshot_as_png(), "失败截图", allure.attachment_type.PNG)
# 设置为session,全部用例执行一次
@pytest.fixture(scope='session')
def driver():
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群看公告就可以啦。
总而言之
自动化测试是一个不断学习不断更新的东西,我们都需要时刻准备接受新的知识!文章来源:https://www.toymoban.com/news/detail-415086.html
PS:最后还是附上我们的[QQ交流群]:516846105 真心希望所有对测试感兴趣,想入门,想提升自己测试能力的小伙伴加入!文章来源地址https://www.toymoban.com/news/detail-415086.html
到了这里,关于Python+Selenium+Pytest+Allure自动化测试框架实战实例(示例为我司网盘产品)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!