webUI自动化实现及封装

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文件夹中的压缩包进行邮件发送;
相关推荐
xx~t2 小时前
嵌入式——Linux软件编程2
linux·运维·服务器·c语言·学习方法
Shell运维手记2 小时前
Linux (挂载|rpm|yum|源码编译)
linux·运维·笔记·网络协议·bash
-今昭-2 小时前
MFS高可用负载均衡
运维·负载均衡
ITyunwei09872 小时前
SSL 证书过期排查实战:openssl + Nginx 4步从发现到续签
运维·网络
郝开3 小时前
Docker Compose 本地环境搭建:kkFileView
运维·docker·容器
QYR_113 小时前
2032年气相分解设备市场规模达2.25亿美元:VPD向高灵敏度、自动化晶圆污染检测升级
运维·自动化
RisunJan3 小时前
Linux命令-vgchange(修改 LVM 卷组属性)
linux·运维·服务器
仓储管理员20253 小时前
不同业务场景下,如何匹配适配的WMS系统?
大数据·运维·云计算·精选
Είναι η κοπέλα3 小时前
游戏窗口自动化点击与防误点设计:模板匹配 + 区域限制 + 强制置顶
运维·游戏·自动化