坐井观天说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
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狂奔的蜗牛x

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值