webUI自动化实现及封装
从用户登录_进入商品详情_提交订单_确认订单_余额支付场景开始
一、实例化webdriver打开chrome浏览器,进入商城
# main.py
import time
import pyperclip
from pykeyboard.windows import PyKeyboard
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("<https://mall.jctesting.cn/pc/>")
open_JcMall() # 处理商城登录权限
driver.maximize_window()
driver.implicitly_wait(5)
二、登录_进入商品详情_余额支付
#在商城首页点击【登录】后进行登录操作
driver.find_element(By.LINK_TEXT,"登录").click()
#在登录页面点击【账号密码登录】
driver.find_element(By.CSS_SELECTOR,"[class='header-tab xxl']").click()
driver.find_element(By.CSS_SELECTOR,"[placeholder='请输入账号/手机号码']").send_keys('15200000001')
driver.find_element(By.CSS_SELECTOR,"[placeholder='请输入密码']").send_keys('111111.')
driver.find_element(By.XPATH,"//*[@id='__layout']/div/div[1]/div/div/div/div/div[1]/div[3]/div[2]/div[2]/button").click()
# 登录后在首页进入第一个商品的详情页面
eles = driver.find_elements(By.CSS_SELECTOR,"[class='goods-item bg-white']")
eles[0].click()
# 在商品详情页面点击【立即购买】进入确认订单页面
driver.find_element(By.CSS_SELECTOR,"[class='btn bg-primary white']").click()
# 在确认订单页面点击【提交订单】进入待支付页面
driver.find_element(By.CSS_SELECTOR, "[class='white bg-primary lg btn row-center']").click()
# 在待支付页面选择【余额支付】
eles = driver.find_elements(By.CSS_SELECTOR,"[class='ml16 xxl']")
eles[0].click()
driver.quit()
对上述代码进行按PO模式进行封装
1、新建一个py_page包
存放base.py模块,以及按页面建模:商城页面对每个页面创建一个.py模块
1.1、封装一个base模块
该模块实现webdriver的初始化,元素定位及元素操作的封装,从yaml中读取元素及行为对元素进行操作来驱动业务
# base.py
import yaml
from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from log import Logger
from common.open_jcmall import open_JcMall
logging = Logger(__name__)
class Base:
_baseurl = "<https://mall.jctesting.cn/pc/>"
def __init__(self,driver:webdriver = None):
if driver is None:
self.driver = webdriver.Chrome()
self.driver.get(self._baseurl)
open_JcMall()
else:
self.driver = driver
self.driver.maximize_window()
self.driver.implicitly_wait(5)
def find(self, by, locator):
by_locator = None
if by == 'id' or by == 'ID':
by_locator = (By.ID, locator)
elif by == 'xpath' or by == 'XPATH':
by_locator = (By.XPATH, locator)
elif by == 'name' or by == 'NAME':
by_locator = (By.NAME, locator)
elif by == 'css_selector' or by == 'CSS_SELECTOR':
by_locator = (By.CSS_SELECTOR, locator)
elif by == 'link_text' or by == 'LINK_TEXT':
by_locator = (By.LINK_TEXT, locator)
WebDriverWait(self.driver, 5).until(EC.presence_of_element_located(by_locator))
ele = self.driver.find_element(*by_locator)
return ele
def finds(self, by, locator):
by_locator = None
if by == 'id' or by == 'ID':
by_locator = (By.ID, locator)
elif by == 'xpath' or by == 'XPATH':
by_locator = (By.XPATH, locator)
elif by == 'name' or by == 'NAME':
by_locator = (By.NAME, locator)
elif by == 'css_selector' or by == 'CSS_SELECTOR':
by_locator = (By.CSS_SELECTOR, locator)
elif by == 'link_text' or by == 'LINK_TEXT':
by_locator = (By.LINK_TEXT, locator)
WebDriverWait(self.driver, 10).until(EC.presence_of_all_elements_located(by_locator))
ele = self.driver.find_elements(*by_locator)
return ele
def find_and_click(self, by, locator):
ele = self.find(by, locator)
ele.click()
def finds_and_click(self, by, locator, index):
eles = self.finds(by, locator)
eles[index].click()
def find_and_send(self, by, locator, text):
self.find(by, locator).send_keys(text)
def find_and_clear(self, by, locator):
self.find(by, locator).clear()
def run_steps(self, yaml_path, operation, sendText: list = None, clickIndex: list = None):
with open(yaml_path, "r", encoding="UTF-8") as f:
datas = yaml.safe_load(f)
steps = datas[operation]
send_index = 0
click_index = 0
for step in steps:
action = step["action"]
if action == "find_and_click":
try:
self.find_and_click(*step["locator"])
except Exception as e:
logging.logger.info(f'{yaml_path}:{action}:{step["locator"]}元素定位异常,错误信息:{e}')
return None
elif action == "find_and_send":
if sendText[send_index]:
try:
self.find_and_send(*step["locator"], sendText[send_index])
send_index += 1
except Exception as e:
logging.logger.info(f'{yaml_path}:{action}:{step["locator"]}元素定位异常,错误信息:{e}')
return None
else:
logging.logger.info("find_and_send 方法中未传入arg参数")
elif action == "find_and_clear":
try:
self.find_and_clear(*step["locator"])
except Exception as e:
logging.logger.info(f'{yaml_path}:{action}:{step["locator"]}元素定位异常,错误信息:{e}')
return None
elif action == "finds":
try:
ele = self.finds(*step["locator"])
return ele
except Exception as e:
logging.logger.info(f'{yaml_path}:{action}:{step["locator"]}元素定位异常,错误信息:{e}')
return None
elif action == "finds_and_click":
if clickIndex != None:
try:
self.finds_and_click(*step["locator"], index=clickIndex[click_index])
click_index += 1
except Exception as e:
logging.logger.info(f'{yaml_path}:{action}:{step["locator"]}元素定位异常,错误信息:{e}')
return None
else:
logging.logger.info("finds_and_click 方法中未传入index参数")
elif action == "find":
try:
ele = self.find(*step["locator"])
return ele
except Exception as e:
logging.logger.info(f'{yaml_path}:{action}:{step["locator"]}元素定位异常,错误信息:{e}')
return None
Base类中的构造方法中有一个webdriver类型的driver参数,当driver为None时,会创建一个新的webdriver实例对象,同时该模块中封装了find( ) 与 finds( ) 方法来实现元素的定位来返回单个元素对象或多个元素的对象集,基于find( ) 与finds( ) 方法再封装了对元素和元素集的常用操作,如click( )、send_keys( )、clear( );run_steps方法从yaml中读取页面元素,根据yaml中记录的元素对应行为,调用find( )、finds( )、find_and_click( )、 finds_and_click( )、find_and_send( )等方法实现对元素的操作
run_steps参数:
- yaml_path: 要读取的yaml文件地址及文件名
- operation: yaml文件中的模块

- sendText: 为list类型,针对yaml文件的operation对应action为find_and_send的行为,即对该定位到的元素发送字符,一个operation支持多个action;
- clickIndex: 为list类型,针对yaml文件的operation对应action为finds_and_click的行为,即对定位到的元素集中某个元素进行点击操作;
1.2、对页面建模
即每个页面就是一个.py文件,在该py文件中把这个页面定义为一个类,该页面类继承base模块的Base类,把该页面对元素操作和实现的功能定义为一个方法;
# main_page.py
import time
from py_page.base import Base
class MainPage(Base):
path = "./yaml_page/main_page.yaml"
def goto_loginPage(self):
self.run_steps(self.path,"login")
from py_page.login_page import LoginPage
return LoginPage(self.driver)
def goto_registerPage(self):
self.run_steps(self.path,"register")
from py_page.register_page import RegisterPage
return RegisterPage(self.driver)
def get_loginName(self):
ele = self.run_steps(self.path,"loginName")
return self, ele.get_attribute("text")
def search_goods(self,goodsName):
self.run_steps(self.path,"searchGoods",[f"{goodsName}"])
self.run_steps(self.path, "searchButton")
goodsNameList = list()
time.sleep(2)
goodsList = self.run_steps(self.path, "searchGoodsList")
for goods in goodsList:
goodsNameList.append(goods.get_attribute("innerHTML"))
return self,goodsNameList
def goto_goodsDetailPage(self,index):
self.run_steps(self.path, "goodsDetail", clickIndex=[index])
from py_page.goodDetail_page import GoodDetailPage
return GoodDetailPage(self.driver)
# login_page.py
import time
from py_page.base import Base
class LoginPage(Base):
path = "./yaml_page/login_page.yaml"
def use_accountLogin(self,account,password):
self.run_steps(self.path,"accountLogin")
self.run_steps(self.path,"inputAccount",sendText=[account])
self.run_steps(self.path, "inputPassword", sendText=[password])
self.run_steps(self.path,"clickLoginButtion")
time.sleep(2)
if self.run_steps(self.path,'alert'):
alert_text = self.run_steps(self.path,'alert').get_attribute("innerHTML")
print(alert_text)
return self,alert_text
else:
from py_page.main_page import MainPage
return self,MainPage(self.driver)
# goodDetail_page.py
from py_page.base import Base
class GoodDetailPage(Base):
path = "./yaml_page/goodsDetail_page.yaml"
def buy_now(self):
self.run_steps(self.path,"buyNow")
from py_page.confirmOrder_page import ConfirmOrderPage
return ConfirmOrderPage(self.driver)
# payOrder_page.py
from py_page.base import Base
class PayOrderPage(Base):
path = "./yaml_page/payOrder_page.yaml"
def pay_way(self,index):
self.run_steps(self.path,"PayWay",clickIndex=[index])
from py_page.payResult_page import PayResultPage
return PayResultPage(self.driver)
# payResult_page.py
import time
from py_page.base import Base
class PayResultPage(Base):
path = "./yaml_page/payResult_page.yaml"
def get_payResult(self):
time.sleep(2)
ele = self.run_steps(self.path,"payResult")
result = ele.get_attribute("innerHTML")
return self,result

编写方法的一个原则:页面中的方法是为测试用例服务的,测试用例中需要用到什么方法,就在对应页面类中定义并返回需要的数据,或跳转到对应页面;
上面场景:登录_进入商品详情_余额支付 页面跳转顺序是:商城首页-登录页面-商城首页-商品详情页面-确认订单页面-支付订单页面-支付结果页面;
对上述场景进行PO分层,按建模思想,对每个页面生成一个py模块,每个模块定义一个页面类并继承Base类,
2、为了页面元素与代码的剥离(适应UI自动化页面元素变化较频繁),新建yaml_page,按页面存放需要用到的页面元素以及对元素的行为

login: # 点击首页【登录】
- action: find_and_click
locator: [LINK_TEXT, "登录"]
register: # 点击首页【注册】
- action: find_and_click
locator: [LINK_TEXT, "注册"]
loginName: # 定位首页登录后的姓名
- action: find
locator: [CSS_SELECTOR, ".el-popover__reference"]
searchGoods: # 向首页搜索框发送字符
- action: find_and_send
locator: [CSS_SELECTOR, "[placeholder='请输入要搜索的商品名称']"]
searchButton: # 点击首页【搜索】
- action: find_and_click
locator: [CSS_SELECTOR, "[class='search-btn bg-primary white row-center']"]
searchGoodsList: # 定位搜索结果集中的所有商品
- action: finds
locator: [ CSS_SELECTOR, "[class='name line2']" ]
goodsDetail: # 进入首页商品详情
- action: finds_and_click
locator: [ CSS_SELECTOR, "[class='goods-item bg-white']" ]
上述各模块调用逻辑:

3、针对上述模块,编写测试用例,新建testCase包
teatCase包中,包含base_test.py:用于封装所有case需要用到的公共方法,每个测试模块可按业务模块区分以test_开头

# base_test.py
from py_page.main_page import MainPage
class BaseTest:
def login_to_main(self,account='15200000001',pwd='111111.'):
ele = MainPage().goto_loginPage().use_accountLogin(account, pwd)
return ele[1]
# test_order.py
import pytest
import allure
from testCase.base_test import BaseTest
from log import Logger
from screen_shot import getScreenShot
logging = Logger(__name__)
@allure.feature("测试订单功能")
class TestOrder(BaseTest):
@allure.story("测试生成订单")
@allure.title("用余额购买商品生成订单")
def test_payOrder(self):
logging.logger.info("*******正在执行测试用例:test_payOrder*******")
with allure.step("登录_进入首页_进入商品详情_点击【立即购买】_点击【提交订单】_选择支付方式支付_获取支付结果"):
ele = self.login_to_main().goto_goodsDetailPage(0).buy_now().submit_order().pay_way(0).get_payResult()
result = ele[1]
logging.logger.info(f"获取到的支付结果为:{result}")
try:
assert result=='支付成功'
except AssertionError as e:
getScreenShot(__name__)
logging.logger.info(f"FAILE*******测试未通过*******:{e}")
raise e
finally:
ele[0].driver.quit()
测试用例中通过调用py_page页面类中的方法,通过链式调用来组织用例步骤
例:ele = self.login_to_main().goto_goodsDetailPage(0).buy_now().submit_order().pay_way(0).get_payResult()
首先调用base_test中的lotin_to_main( )用户登录,用户登录后goto_DetailPage(0),即进入首页第一个商品的详情页面调用buy_now( )立即购买,然后submit_order( )为确认订单页面的提交订单,在待支付页面pay_way(0)选择第一种支付方式即余额支付进行支付,支付完成后通过get_payResult( )获取支付结果。
注:get_payResult( ) 最后返回的ele对象是一个元祖(self, result),self: 即指向PayResultPage类的实例,我们在测试用例中需要调用该PayResultPage类的实例方法都可以通过ele0.XXX来完成(XXX为该类下的实例方法,由于PayResultPage类继承了Base页面基类,也可通过该方法调用Base中的实例方法或属性);
测试用例结束后ele0.driver.quit( )关闭浏览器;
测试用例的单例设计模式:每条测试用例执行首先实例化一个webdriver(每次实例化一个webdriver都会生成一个独立的sessionid),在py_page的页面类中涉及页面跳转的都会return 一个页面类,该类中需要传入一个self.driver的参数,被Base类接收处理,来保证在测试用例链式调用中页面跳转后是同一个webdriver实例,测试用例执行结束通过self.driver.quit( )来关闭这个webdriver释放资源;
4、创建common包,存放框架的需要被引用的常用模块,该包中包含日志模块(log.py),截图模块(screen_shot.py),发送邮件模块(send_mail.py),压缩文件模块(zip_file.py)
4.1、引用已经封装好的log.py模块
在common目录下放入log.py模块
# log.py
import logging
import time
import os
class Logger:
def __init__(self,name,CmdLevel=logging.INFO, FileLevel=logging.INFO):
# 创建一个 logger
self.logger = logging.getLogger(name)
# 设置Log等级总开关
self.logger.setLevel(CmdLevel)
# 创建 handler,用于日志写入
currTime = time.strftime("%Y-%m-%d")
log_path = os.path.abspath(os.path.join(os.getcwd(), ".")) + '/Logs/'
log_name = log_path + currTime + '.log'
fh = logging.FileHandler(log_name, mode='a') # mode = 'a' 为在原日志上追加,'w'为覆盖 输出日志到文件
fh.setLevel(FileLevel) # 输出到 file 的日志等级
ch = logging.StreamHandler() # 输出日志到控制台
ch.setLevel(FileLevel) # 输出到控制台的日志等级
# 定义日志handler的输出格式
fmt = logging.Formatter('%(asctime)s - %(filename)s:[%(lineno)s] - [%(levelname)s] - %(message)s')
fh.setFormatter(fmt)
ch.setFormatter(fmt)
# 添加 logger 到 handler里面
self.logger.addHandler(fh)
self.logger.addHandler(ch)
log模块在各其他模块中import后通过logging = Logger(name ) 进行实例化,name :python的内置类属性,主要存储模块的名称,python的模块既可以被调用,也可以独立运行。而被调用时__name__存储的是py文件名(模块名称),独立运行时存储的是"main"。
4.2、增加错误截图模块 screen_shot.py
在测试用例中import截图片模块,在断言失败后调用该模块的getScreenShot( )方法实现错误页面的截图
# screen_shot.py
import os
import allure
import pyscreenshot as ImageGrab
import time
currTime = time.strftime("%m月%d日%H时%M分%S秒")
def getScreenShot(funcName):
filename= os.path.abspath(os.path.join(os.getcwd(), ".")) + '\Screen_Shot\\'+ funcName+' '+ currTime +'.jpg'
im = ImageGrab.grab()
im.save(filename)
im.close()
allure.attach.file(source=filename, name=f'{funcName}+" "+{currTime}', attachment_type=allure.attachment_type.JPG)
通过调用 allure.attach.file( ) 方法把截图追加到allure测试报告中与测试报告集成;
4.3、增加 send_mail.py 模块,实现测试结果的邮件发送
# send_mail.py
import smtplib
from email.header import Header
from email.mime.application import MIMEApplication
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
class SendEmail:
# 初始化服务器信息
def __init__(self,mail_host, mail_user, mail_pass, sender, receives):
self.mail_host = mail_host
self.mail_user = mail_user
self.mail_pass = mail_pass
self.sender = sender
self.receivers = receives
# 以文本的形式发送邮件
def make_email_by_text(self,context, subject, from_address, to_address):
message = MIMEText(context, 'plain', 'UTF-8')
message['Subject'] = subject # 邮件标题
message['From'] = Header(from_address, "utf-8") # 邮件主体中发送者名称
message['To'] = Header(to_address, "utf-8") # 邮件主体中接收者名称
self.send_email(message)
# 以文本和附件的形式发送邮件
def make_email_by_att(self,content,file_path,subject, from_address, to_address):
message = MIMEMultipart()
message['Subject'] = subject # 邮件标题
message['From'] = Header(from_address, "utf-8") # 邮件主体中发送者名称
message['To'] = Header(to_address, "utf-8") # 邮件主体中接收者名称
body = MIMEText(content,'plain','utf-8')
message.attach(body)
att = MIMEApplication(open(file_path, 'rb').read())
att.add_header('Content-Disposition','attachment',filename='allure测试报告.zip')
message.attach(att)
self.send_email(message)
# 登录并进行发送
def send_email(self,message):
# 进行登录发送
try:
smtpobj = smtplib.SMTP_SSL(self.mail_host)
smtpobj.ehlo(self.mail_host)
smtpobj.login(self.mail_user,self.mail_pass)
smtpobj.sendmail(self.sender,self.receivers,message.as_string())
smtpobj.quit()
print('success')
except Exception as e:
print(f'error: {e}')
raise e
4.4、zip_file.py模块实现把allure测试报告进行压缩,在report_zip文件夹中生成report.zip文件,作为send_mail.py邮件的附件打包发送
# zip_file.py
import os
import zipfile
from common.log import Logger
logging = Logger(__name__)
def zip_files(dir_path,zip_path):
'''
:param dir_path: 需要压缩的文件夹地址 例:'D:\Buyer_test_code\apipotest_jcmall\report'
:param zip_path: 压缩后的文件存在地址+压缩文件名 例:'D:\Buyer_test_code\apipotest_jcmall\report_zip\report.zip'
:return: 0 压缩失败 1 压缩成功
'''
try:
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as f:
for dirpath, dirnames, filenames in os.walk(dir_path):
fpath = dirpath.replace(dir_path, '')
fpath = fpath and fpath + os.sep or ''
for filename in filenames:
f.write(os.path.join(dirpath, filename), fpath+filename)
logging.logger.info(f"文件:{dir_path} 压缩成功")
except Exception as e:
logging.logger.error(f"文件:{dir_path} 压缩失败,错误:{e}")
return 0
else:
return 1
5、在项目根目录下创建一个run.py模块,作为整个框架的执行入口模块
# run.py
from common.log import Logger
from common.zip_file import zip_files
logging = Logger(__name__)
def run_testCase():
test_allure = "python -m pytest ./testCase/ -s -q --alluredir=./result/"
logging.logger.info(f"run_testCase::正在执行测试命令:{test_allure}")
try:
os.system(test_allure)
except Exception as e:
logging.logger.error(f"os执行命令{test_allure}错误:{e}")
else:
make_report = "allure generate ./result/ -o ./report/ --clean"
try:
logging.logger.info(f"run_testCase::测试执行完毕,正在执行生成报告命令:{make_report}")
os.system(make_report)
except Exception as e:
logging.logger.error(f"os执行命令{make_report}错误:{e}")
else:
dir_path = r'./report'
zip_path = r'./report_zip/report.zip'
zip_report = zip_files(dir_path,zip_path)
if zip_report:
file_name = r'./config.yaml'
file_path = zip_path
content = "测试执行结束,Allure测试报告在邮件附件中,请使用Pycharm打开查看。"
Subject = 'webUI自动化测试报告' # 邮件标题
From = "Jie的QQ邮箱" # 邮件主体中发送者名称
To = "Jie的163邮箱" # 邮件主体中接收者名称
try:
with open (file_name,"r",encoding="UTF-8") as f:
config = yaml.safe_load(f)
config_email = config["Email"]
send = SendEmail(mail_host=config_email['mail_host'], mail_user=config_email['mail_user'], mail_pass=config_email['mail_pass'],
sender=config_email['sender'], receives=config_email['receives'])
send.make_email_by_att(content,file_path, Subject, From, To)
logging.logger.info(f"邮件发送成功,邮件标题:{Subject}")
except Exception as e:
logging.logger.error(f"读取配置文件::{file_name}::失败::{e}")
raise e
该模块作为一个框架执行入口实现以下功能:
- 通过os.system( )函数调用系统命令行执行:"python -m pytest ./testCase/ -s -q --alluredir=./result/" 即通过pytest执行 testCase文件夹的所有以test开头的测试用例,并在result文件夹下生成测试结果数据;
- 通过os.system( ) 函数调用系统命令行执行:"allure generate ./result/ -o ./report/ --clean" 即根据result文件夹中的结果数据在report文件夹中生成allure测试报告;
- 调用zip_file.py模块中的zip_files( ) 方法对allure测试报告report文件夹进行压缩打包后存放report_zip文件夹中;
- 实例化send_mail.py模块中的SendEmail类对report_zip文件夹中的压缩包进行邮件发送;