坐井观天说Devops--4--测试CICD之k8s部署selenium分布式自动化持续集成
一.介绍
本篇博客主要实现了,因为gitlab和jenkins做了关联,登录jenkins网站,选择任意一个测试脚本的版本,并且能够选择一些测试脚本的参数,然后可以将参数注入到测试镜像中去。然后将测试脚本的镜像自动的部署到k8s集群中去,测试完成之后,会有allure测试结果,显示在构建的界面。
搭建的测试CICD持续交付的框架已经搭建完成了。框架主要主要使用了:
开发语言:python、shell
开发环境:pycharm
代码管理:gitlab
镜像制作:dockerfile
镜像管理:harbor
构建工具:jenkins
测试报告:allure
测试框架:selenium
测试用例管理:pytest
测试用例架构设计:po模式
集群管理工具:k8s
测试代码项目部署:以docker容器使用k8s的job方式部署
测试代码项目的配置管理:configMap
测试代码项目的数据持久化:storageClass
jenkins slave节点:以docker容器使用k8s的方式部署
二.解决的问题场景
费了很大的力气,才将这篇博客写完,那么我们这篇博客,主要解决问题的场景是什么?
场景1:我们需要500个浏览器进行测试,运行相同的测试脚本,我们不可能找500个电脑装500个浏览器和浏览器driver,目前搭建的这套架构,能够解决这方面的问题(目前我们公司就有这方面的需求能够解决这方面的问题)
场景2:兼容性测试,能够动态的选择不同的浏览器(后面可以加上不同的浏览器版本镜像),选择不同的测试代码版本进行测试
场景3:能够配置job矩阵,当开发将应用部署上去之后,接着执行构建后的操作,可以将该job加入进去,以自动实现快速的自动的给出测试结果
还有什么好处:
1.通过jenkins,能够可视化整个测试流程
2.通过jenkins,能够将一些关键参数(如:测试url,浏览器选择,个数,用例级别)注入到我们的测试脚本中,当然,目前
这些关键参数,可能远远不够,这个需要根据公司的实际项目进行进化,目前仅仅是demo,抛砖引玉
3.首先,对于大规模的测试,肯定是需要很多机器进行集群,那么对于测试脚本的集群部署,还有对于镜像的动态制作生成,回退脚本镜像,选择不同的浏览器啥的,还有很多个pod容器的allure测试报告整合,这些中间动作,全部自动化掉,对于
测试工程师,我们只需要专注于我们的测试代码,而不是需要去手动的去执行。而且也不需要测试工程师,去学习k8s,jenkins啥的。
4.更简单,测试工程师开发或者修改测试脚本或者回退版本,然后提交到gitlab后,然后登录到jenkins网站,直接根据自己的需求,选择一些参数,直接build,然后就是静静的等待allure测试报告(目前邮件或微信或钉钉通知没有做,会后面的章节去做的)
PS:
这些都是我粗浅的见解,不喜勿喷
三.测试CICD整体流程
1.(手动)在pycharm上调试代码,调试完成后,通过git将代码上传到gitlab上
2.(手动)登录jenkins网站,创建对目标网站的测试,并且能选择一些参数,动态的注入到测试脚本中去,如下图
3.(自动)jenkins的slave节点,会自动的下载测试脚本的代码,然后打包成docker镜像,然后推送到harbor网站
4.(自动)jenkins的slave节点,会启动job,进行对目标网站的测试
5.(自动)测试完成之后,会自动整合不同pod的测试结果,并且整合成一个allure测试报告
6.(手动)等测试完成之后,手动进入jenkins网站,进行查看allure测试报告(关于微信,邮件,钉钉等的通知,本次没有做)
四.UI自动化架构设计
1.脚本开发环境准备
1.在Ubuntu desktop的电脑安装pycharm,可以参考博客,在Ubuntu中安装Pycharm(Ubuntu21.10,Pycharm2021.1.3)
2.Ubuntu desktop电脑安装selenium,pytest,allure,这些安装,可以文考我博客上关于制作selenium的Chrome镜像,里面,关于安装这些都会涉及
3.在我搭建的gitlab(gitlab.xusanduo.com)上,新建一个项目,用来保存po设计模式的代码,具体怎么操作,这里不赘述
4.将搭建的项目uitestdemo和gitlab上的uitestdemo进行关联
代码上传成功后,gitlab截图
2.PO设计模式
花了1个周多,我真是太能折腾了,根据博客,基于Selenium与Pytest框架的Web UI自动化测试系统的设计与实现,编写了一个百度网站的ui自动化测试框架,在博客的基础上,增加了一些功能。
a.关于ui自动化框架的设计思路和具体实施
对于一个优秀的框架,不可或缺的当属是分层思想,而在Web UI自动化测试中,PO模式即Page Object是十分流行的一项技术了。PO是一种设计模式,提供了一种页面元素定位和业务操作流程分离的模式。当页面元素发生变化时,只需要维护对应的page层修改定位,不需要修改业务逻辑代码。
PO核心思想是分层,实现脚本重复使用,易维护,可读性高,主要分三层:
对象库层:Base(基类),封装page 页面一些公共的方法,如初始化方法、查找元素方法、点击元素方法、输入方法、获取文本方法、截图方法等。
操作层:page(页面对象),封装对元素的操作,一个页面封装成一个对象
业务层:business(业务层),将一个或多个操作组合起来完成一个业务功能。比如登录:需要输入帐号、密码、点击登录三个操作。
基于分层思想和PO设计模式,我们可以设计出如下基本的框架模型:
cases测试用例层:存放所有的测试用例
common公共层:存放一些公共的方法,如封装page页面基类、捕获日志等
datas测试数据层:存放测试数据,用yaml文件进行管理
logs日志层:存放捕获到的所有日志和错误日志,便于问题定位
pages页面对象层:存放所有页面对象,一个页面封装成一个对象
reports测试报告层:存放产出的测试结果数据,失败截图
关于basepage的代码如下:
base.py
import re
import time
import allure
from selenium import webdriver
from common.wrapper import handle_black
from common.utils import get_logger, get_config_data
class BasePage:
_params = {
}
_driver = None
#放置这里,后续所有page和测试用例,都可以使用到这个日志logger,来记录log
logger = get_logger()
#用来读取配置文件
config = get_config_data()
#初始化driver
def __init__(self, driver: webdriver = None):
self._driver = driver
#driver隐私等待
def set_implicitly(self, wait_time):
self._driver.implicitly_wait(wait_time)
#用来截图
def screenshot(self, name):
self._driver.get_screenshot_as_file(name)
#用来截图,并且,能够将截图导入到测试用例中
def allure_screenshot(self, filename, file_path):
self.screenshot(file_path)
with open(file_path, "rb") as f:
content = f.read()
allure.attach(content, name=filename, attachment_type=allure.attachment_type.PNG)
#用来切换tab页面
def switch_to_window(self, index):
handles = self._driver.window_handles
self._driver.switch_to.window(handles[index])
#用来保持浏览器,可以保留那些tab页面
def keep_windows(self, keep_windows_tuple: tuple):
handles = self._driver.window_handles
if len(handles) <= len(keep_windows_tuple):
raise ValueError
for index in range(len(handles)):
if not keep_windows_tuple.__contains__(index):
self.switch_to_window(index)
self._driver.close()
#用来获取元素列表,handle_black装饰器用来处理弹窗以及异常
@handle_black
def finds(self, locator, value: str = None):
elements: list
if isinstance(locator, tuple):
elements = self._driver.find_elements(*locator)
else:
elements = self._driver.find_elements(locator, value)
return elements
#用来获取单个元素,handle_black装饰器用来处理弹窗以及异常
@handle_black
def find(self, locator, value: str = None):
if isinstance(locator, tuple):
element = self._driver.find_element(*locator)
else:
element = self._driver.find_element(locator, value)
return element
#用来通过text文本来获取元素
@handle_black
def find_and_get_text(self, locator, value: str = None):
if isinstance(locator, tuple):
element_text = self._driver.find_element(*locator).text
else:
element_text = self._driver.find_element(locator, value).text
return element_text
#通过js脚本,获取元素后,并且点击
def find_js_click(self, ele):
self._driver.execute_script('arguments[0].click();', ele)
#通过js脚本,来上下滑动窗口
def window_vertical_scroll_to_by_js(self, height_start=0,
height_stop='document.body.scrollHeight', scroll_to_nums=1):
for i in range(scroll_to_nums):
if height_stop == 1:
height_stop = 'document.body.scrollHeight'
self._driver.execute_script(f'window.scrollTo({
height_start}, {
height_stop})')
#通过判断text文本是否在pagesource中
def check_text_in_page(self, text: str, page: str):
target_string = re.sub('[./]+', '', text)
target_string_sub_list = re.split('[^\u4e00 -\u9fa5]+', target_string)
print('target_string', target_string)
print('target_string_sub_list', target_string_sub_list)
for target_string_sub in target_string_sub_list:
if not page.__contains__(target_string_sub):
return False
return True
#在元素执行动作之前,需要做那些准备操作
def before_exec_action_prepare(self, before_exec_action):
if 'switch_window' in before_exec_action:
self.switch_to_window(before_exec_action['switch_window'])
if 'scroll_vertical_to' in before_exec_action:
self.window_vertical_scroll_to_by_js(**before_exec_action['scroll_vertical_to'])
#执行动作前,需要提前调整执行动作后检查点参数,以应对元素动作执行后的检查点操作
def check_points_params_process(self, after_exec_action, element):
print('check_points----------', after_exec_action)
print('element---------------', element.text)
if "check_points" in after_exec_action:
check_points = after_exec_action['check_points']
for index in range(len(check_points)):
if check_points[index]['type'] == 'text_in_page':
if check_points[index]['is_action_element'] == 1:
text = element.text
check_points[index]['text'] = text
return after_exec_action
#执行元素动作后的检查点操作
def check_points_process(self, after_exec_action):
if "check_points" in after_exec_action:
for check in after_exec_action['check_points']:
if check['type'] == 'text_in_page':
if check['text'].count("${") == 1 and check['is_action_element'] == 0:
element = self.find(check['element']['by'], check['element']['locator'])
time.sleep(2)
page = self._driver.page_source
text = element.text
self.allure_screenshot('before_assert_screenshot',
self.config['screenshots_path'] + 'assert.PNG')
assert self.check_text_in_page(text, page) == True
else:
time.sleep(2)
page = self._driver.page_source
self.allure_screenshot('before_assert_screenshot',
self.config['screenshots_path'] + 'assert.PNG')
assert self.check_text_in_page(check['text'], page) == True
#执行元素动作后的操作
def after_exec_action_process(self, after_exec_action):
if "switch_window" in after_exec_action:
self.switch_to_window(after_exec_action["switch_window"])
if "sleep" in after_exec_action:
time.sleep(after_exec_action["sleep"])
self.check_points_process(after_exec_action)
if "is_screenshot" in after_exec_action:
if after_exec_action['is_screenshot'] == 1:
screenshot_path = self.config['screenshots_path'] + after_exec_action['screenshot_name'] + '.PNG'
self.allure_screenshot(after_exec_action["screenshot_name"], screenshot_path)
#处理测试用例的步骤
def process_steps(self, steps):
for step in steps:
self.before_exec_action_prepare(step['before_exec_action'])
if "action" in step.keys():
action = step["action"]
if "index" in step["locator"]:
element = self.finds(step["locator"]["by"],
step["locator"]["locator"])[int(step["locator"]["index"])]
else:
element = self.find(step["locator"]["by"], step["locator"]["locator"])
step['after_exec_action'] = self.check_points_params_process(step['after_exec_action'], element)
print("link is :" + element.get_attribute('href'))
if "click" == action:
element.click()
if "send" == action:
element.send_keys(step["value"])
self.after_exec_action_process(step['after_exec_action'])
#浏览器的返回操作
def back(self):
self._driver.back()
关于用来处理弹窗的装饰器函数如下
wrapper.py
import logging
import allure
from selenium.webdriver.common.by import By
def handle_black(func):
logging.basicConfig(level=logging.INFO)
def wrapper(*args, **kwargs):
from pages.base.base import BasePage
_black_list = [
(By.XPATH, "//*[@id='test']"),
(By.XPATH, "//*[@text='test1']"),
(By.XPATH, "//*[@text='test2']"),
(By.XPATH, "//*[@text='test3']"),
]
_max_num = 3
_error_num = 0
instance: BasePage = args[0]
try:
logging.info("run " + func.__name__ + "\n args: \n" + repr(args[1:]) + "\n" + repr(kwargs))
element = func(*args, **kwargs)
print(element, '========================')
_error_num = 0
# 隐式等待回复原来的等待,
instance._driver.implicitly_wait(10)
return element
except Exception as e:
instance.screenshot("tmp.png")
with open("tmp.png", "rb") as f:
content = f.read()
allure.attach(content, attachment_type=allure.attachment_type.PNG)
logging.error("element not found, handle black list")
instance._driver.get_screenshot_as_png()
instance._driver.implicitly_wait(1)
# 判断异常处理次数
if _error_num > _max_num:
raise e
_error_num += 1
# 处理黑名单里面的弹框
for ele in _black_list:
elelist = instance.finds(*ele)
if len(elelist) > 0:
elelist[0].click()
# 处理完弹框,再将去查找目标元素
return wrapper(*args, **kwargs)
raise e
return wrapper
关于用来打开浏览器,进入到百度应用首页的代码如下
app.py
import selenium.webdriver.common.devtools.v85.profiler
from selenium.webdriver.common.by import By
from pages.base.base import BasePage
from selenium import webdriver
from selenium.webdriver.chrome.options import Options as ChromeOption
from selenium.webdriver.firefox.options import Options as FirefoxOption
from selenium.webdriver.edge.options import Options as EdgeOption
from pages.base.base import BasePage
class App(BasePage):
def start(self):
if self._driver is None:
options = globals()[self.config['browser']['options']]()
for content in self.config['browser']['optionsContent']:
options.add_argument(content)
self._driver = getattr(webdriver, self.config['browser']['type'], None)(options=options)
self._driver.get(self.config['baseUrl'])
self._driver.maximize_window()
self._driver.implicitly_wait(3)
else:
self._driver.launch_app()
self.allure_screenshot('main_page', self.config['screenshots_path'] + 'main_page.PNG')
return self
def restart(self):
self.stop()
self.start()
def stop(self):
self._driver.quit()
def back(self):
self._driver.back()
def main(self):
from pages.main.main import Main
return Main(self._driver)
关于在百度首页,可以进入到各个子模块的main代码如下
main.py
from pages.base.base import BasePage
from common.utils import get_page_data
class Main(BasePage):
main_data = get_page_data('main')
def goto_news(self):
from pages.news.news import News
self.process_steps(self.main_data['news'][0]['steps'])
return News(self._driver)
def goto_hao123(self):
from pages.news.news import News
self.process_steps(self.main_data['hao123'][0]['steps'])
return News(self._driver)
def goto_map(self):
from pages.news.news import News
self.process_steps(self.main_data['map'][0]['steps'])
return News(self._driver