webUI自动化实现及封装
2026/8/21 8:06:02 网站建设 项目流程

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类的实例方法都可以通过ele[0].XXX来完成(XXX为该类下的实例方法,由于PayResultPage类继承了Base页面基类,也可通过该方法调用Base中的实例方法或属性);

测试用例结束后ele[0].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文件夹中的压缩包进行邮件发送;

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询