一.介绍

本篇博客主要实现了,因为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)

    def goto_tieba(self):
        from pages.news.news import News
        self.process_steps(self.main_data['tieba'][0]['steps'])
        return News(self._driver)

    def goto_videos(self):
        from pages.news.news import News
        self.process_steps(self.main_data['videos'][0]['steps'])
        return News(self._driver)

    def goto_images(self):
        from pages.news.news import News
        self.process_steps(self.main_data['images'][0]['steps'])
        return News(self._driver)

    def goto_netdisk(self):
        from pages.news.news import News
        self.process_steps(self.main_data['netdisk'][0]['steps'])
        return News(self._driver)

    def goto_more(self):
        from pages.news.news import News
        self.process_steps(self.main_data['news'][0]['steps'])
        return News(self._driver)

关于新闻主页的一些common函数如下,主要让用测试用列来调用的
news.py

from pages.base.base import BasePage
from common.utils import get_page_data, yaml_content_param_replace
from pages.main.main import Main


class News(BasePage):
    news_data = get_page_data('news')

    def click_a_appoint_new_on_special_news_item(self, news_item, index):
        step = yaml_content_param_replace(self.news_data[news_item][0]['steps'], param_dict={'index': str(index)})
        self.process_steps(step)

    def goto_main_from_news(self):
        self.steps(self.news_data['wangye'])
        return Main(self._driver)

    def goto_tieba_from_news(self):
        self.steps(self.news_data['tieba'])
        return Main(self._driver)

    def goto_zhidao_from_news(self):
        self.steps(self.news_data['zhidao'])
        return Main(self._driver)

    def goto_music_from_news(self):
        self.steps(self.news_data['music'])
        return Main(self._driver)

    def goto_images_from_news(self):
        self.steps(self.news_data['images'])
        return Main(self._driver)

    def goto_videos_from_news(self):
        self.steps(self.news_data['videos'])
        return Main(self._driver)

    def goto_map_from_news(self):
        self.steps(self.news_data['map'])
        return Main(self._driver)

    def goto_wenku_from_news(self):
        self.steps(self.news_data['wenku'])
        return Main(self._driver)

测试用列
test_news.py

import allure
from common.utils import get_test_page_data
from pages.base.app import App
import pytest

page_test_data = get_test_page_data('test_news')


@allure.epic(page_test_data['Test_news']['allure']['epic'])
class Test_news:
    def setup_class(self):
        with allure.step(page_test_data['Test_news']['setup_steps'][0]):
            self.app = App().start()
            self.news = self.app.main().goto_news()

    def setup(self):
        with allure.step(page_test_data['Test_news']['setup_steps'][1]):
            self.news.switch_to_window(1)

    @allure.feature(page_test_data['test_click_one_hot_new']['allure']['feature'])
    @allure.story(page_test_data['test_click_one_hot_new']['allure']['story'])
    @pytest.mark.parametrize('index', page_test_data['test_click_one_hot_new']['select_index_new_click'])
    @pytest.mark.highs
    def test_click_one_hot_new(self, index):
        with allure.step(page_test_data['test_click_one_hot_new']['allure']['steps'][0]):
            self.news.click_a_appoint_new_on_special_news_item(
                page_test_data['test_click_one_hot_new']['news_item'], index)

    @allure.feature(page_test_data['test_click_one_local_new']['allure']['feature'])
    @allure.story(page_test_data['test_click_one_local_new']['allure']['story'])
    @pytest.mark.parametrize('index', page_test_data['test_click_one_local_new']['select_index_new_click'])
    @pytest.mark.lows
    def test_click_one_local_new(self, index):
        with allure.step(page_test_data['test_click_one_local_new']['allure']['steps'][0]):
            self.news.click_a_appoint_new_on_special_news_item(
                page_test_data['test_click_one_local_new']['news_item'], index)

    def teardown(self):
        with allure.step(page_test_data['Test_news']['teardown_steps'][0]):
            self.news.keep_windows((0, 1))

    def teardown_class(self):
        self.app.stop()


配置文件,可以选择使用那种浏览器,使用什么url等来测试,所有的路径相关的文件,都配置到这里
config.yaml

baseUrl: 'https://www.baidu.com'
browser:
  #browser only support Chrome, Firefox, Edge
  type: Chrome
  #option only support ChromeOption, FirefoxOption, EdgeOption
  options: ChromeOption
  optionsContent:
    - '--headless'
    - '--disable-gpu'
    - '--no-sandbox'
    - '--ignore-certificate-errors'
    - '--window-size=1920,1080'

log_config:
  #log config file path
  log_config_file_path: './config/logging.conf'
  #log output type,consolelog or filelog
  log_output_type: 'filelog'

page_data_path:
  main: './data/main.yaml'
  news: './data/news.yaml'

page_test_data_path:
  test_news: './data/test_news.yaml'

screenshots_path: './reports/screenshots/'

关于日志的配置文件,可以调整日志的方式以及日志的输出格式等等
logging.conf

[loggers]
keys=root,consolelog,filelog

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_consolelog]
level=DEBUG
handlers=consoleHandler
qualname=consolelog
propagate=0

[logger_filelog]
level=DEBUG
handlers=filelogHandler,filelogerrorHandler
qualname=filelog
propagate=0

[handlers]
keys=consoleHandler,filelogHandler,filelogerrorHandler

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=logformate
args=(sys.stdout,)

[handler_filelogHandler]
class=FileHandler
level=DEBUG
formatter=logformate
args=('./logs/all.log','a')

[handler_filelogerrorHandler]
class=FileHandler
level=ERROR
formatter=logformate
args=('./logs/error.log','a')

[formatters]
keys=logformate


[formatter_logformate]
format=%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(funcName)s: %(message)s
datefmt=%Y-%m-%d %H:%M:%S

这个文件,主要是main.page来使用的,主要存储一些元素的操作
main.yaml

news:
  - caseName: goto_news_page
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: 0
        locator:
          by: xpath
          locator: '//*[@id="s-top-left"]//*'
          index: 0
        action: click
        after_exec_action:
          switch_window: 1
          sleep: 1
          is_screenshot: 1
          screenshot_name: main_news
hao123:
  - caseName: goto_hao123_page
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="s-top-left"]//*'
          index: 1
        action: click
        after_exec_action:
          switch_window: -1
          is_screenshot: 1
          screenshot_name: main_hao123
map:
  - caseName: goto_map_page
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="s-top-left"]//*'
          index: 2
        action: click
        after_exec_action:
          switch_window: -1
          is_screenshot: 1
          screenshot_name: main_map
tieba:
  - caseName: goto_tieba_page
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="s-top-left"]//*'
          index: 3
        action: click
        after_exec_action:
          switch_window: -1
          is_screenshot: 1
          screenshot_name: main_tieba
videos:
  - caseName: goto_videos_page
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="s-top-left"]//*'
          index: 4
        action: click
        after_exec_action:
          switch_window: -1
          is_screenshot: 1
          screenshot_name: main_videos
images:
  - caseName: goto_images_page
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="s-top-left"]//*'
          index: 5
        action: click
        after_exec_action:
          switch_window: -1
          is_screenshot: 1
          screenshot_name: main_images
netdisk:
  - caseName: goto_netdisk_page
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="s-top-left"]//*'
          index: 6
        action: click
        after_exec_action:
          switch_window: -1
          is_screenshot: 1
          screenshot_name: main_netdisk
more:
  - caseName: goto_more_page
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="s-top-left"]//*'
          index: 7
        action: click
        after_exec_action:
          switch_window: -1
          is_screenshot: 1
          screenshot_name: main_more

这个是news的page界面使用的
news.yaml

hot_news:
  - caseName: get_hot_news
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: 1
        locator:
          by: xpath
          locator: '//*[@id="focus-top"]//a[contains(@href, "http")]'
          index: '${index}'
        action: click
        after_exec_action:
          switch_window: 2
          sleep: 5
          check_points:
            - type: text_in_page
              text: '${text}'
              is_action_element: 1
              element:
                by: xpath
                locator: '//*[@id="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"]'
              timeout: 10
          is_screenshot: 1
          screenshot_name: news_hot_news
local_news:
  - caseName: get_local_news
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: 1
        locator:
          by: xpath
          locator: '//*[@id="local_news"]//a[contains(@href, "http")]'
          index: '${index}'
        action: click
        after_exec_action:
          switch_window: 2
          sleep: 5
          check_points:
            - type: text_in_page
              text: '${text}'
              is_action_element: 1
              element:
                by: xpath
                locator: '//*[@id="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"]'
              timeout: 10
          is_screenshot: 1
          screenshot_name: news_local_news
guonei:
  - caseName: get_guonei_news
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="guonei"]//a[contains(@href, "http")]'
          index: '${index}'
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: news_guonei_news
guoji:
  - caseName: get_guoji_news
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="guojie"]//a[contains(@href, "http")]'
          index: '${index}'
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: news_guoji_news
sports:
  - caseName: get_tiyu_news
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="tiyu"]//a[contains(@href, "http")]'
          index: '${index}'
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: news_tiyu_news
finance:
  - caseName: get_guoji_news
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="caijing"]//a[contains(@href, "http")]'
          index: '${index}'
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: news_caijing_news
technology:
  - caseName: get_technology_news
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="col-tech"]//a[contains(@href, "http")]'
          index: '${index}'
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: news_technology_news
army:
  - caseName: get_junshi_news
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="junshi"]//a[contains(@href, "http")]'
          index: '${index}'
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: news_junshi_news
internet:
  - caseName: get_hulianwang_news
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="hulianwang"]//a[contains(@href, "http")]'
          index: '${index}'
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: news_hulianwang_news
discovery:
  - caseName: get_discovery_news
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="col-discovery"]//a[contains(@href, "http")]'
          index: '${index}'
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: news_discovery_news
lady:
  - caseName: get_lady_news
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="col-lady"]//a[contains(@href, "http")]'
          index: '${index}'
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: news_lady_news
healthy:
  - caseName: get_healthy_news
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="col-healthy"]//a[contains(@href, "http")]'
          index: '${index}'
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: news_healthy_news
picture:
  - caseName: get_tupian_news
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="tupianxinwen"]//a[contains(@href, "http")]'
          index: '${index}'
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: news_tupian_news

wangye:
  - caseName: goto_main_from_news_page
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="header-link-wrapper"]//li'
          index: 0
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: goto_main_from_news_page

tieba:
  - caseName: goto_tieba_from_news_page
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="header-link-wrapper"]//li'
          index: 2
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: goto_tieba_from_news_page
zhidao:
  - caseName: goto_zhidao_from_news_page
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="header-link-wrapper"]//li'
          index: 3
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: goto_zhidao_from_news_page
misic:
  - caseName: goto_music_from_news_page
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="header-link-wrapper"]//li'
          index: 4
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: goto_zhidao_from_news_page
images:
  - caseName: goto_images_from_news_page
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="header-link-wrapper"]//li'
          index: 5
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: goto_images_from_news_page
video:
  - caseName: goto_video_from_news_page
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="header-link-wrapper"]//li'
          index: 6
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: goto_video_from_news_page
map:
  - caseName: goto_map_from_news_page
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="header-link-wrapper"]//li'
          index: 7
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: goto_map_from_news_page
wenku:
  - caseName: goto_wenku_from_news_page
    steps:
      - stepName: step1
        before_exec_action:
          switch_window: -1
        locator:
          by: xpath
          locator: '//*[@id="header-link-wrapper"]//li'
          index: 8
        action: click
        after_exec_action:
          switch_window: -1
          sleep: 5
          is_screenshot: 1
          screenshot_name: goto_wenku_from_news_page

这个主要是test-news.py使用的
test_news.yaml

Test_news:
  allure:
    epic: "百度网站pc端web测试项目"
  setup_steps:
    - '进入百度网站首页和新闻主界面'
    - '测试用列初始化'
  teardown_steps:
    - '测试完成,删除打开的windows,仅仅保留百度网站主界面和新闻主界面'

test_click_one_hot_new:
  news_item: 'hot_news'
  select_index_new_click:
    - 1
    - 2
    - 15
    - 8
    - 12
  allure:
    feature: '新闻主界面测试'
    story: '测试点击热点新闻场景'
    steps:
      - '1.在新闻主界面,选择一个热点新闻'
test_click_one_local_new:
  news_item: 'local_news'
  select_index_new_click:
    - 4
    - 11
    - 9
    - 13
    - 2
  allure:
    feature: '新闻主界面测试'
    story: '测试点击本地新闻场景'
    steps:
      - '1.在新闻主界面,选择一个本地新闻'

b.基于博客基础上的设计,我优化了那些:

1.优化了log,让logger的应用和配置进行分离
Python logging日志模块 配置文件方式
python logging模块详解
官方网站
2.将配置文件和程序进行分离,可以非常方便的调整使用哪款浏览器和设置url
3.测试用例获取driver的方式
4.关于steps的处理更为精细,博客里的代码只是元素动作执行,但是元素动作的执行,其实应该,还需要包括动作执行前的操作,比如是否需要切换窗口,是否需要滑动屏幕等等,动作执行后,也有一系列的操作,比如,是否截图,是否验证,验证点的类型应该也有很多,是否需要切换窗口等等
5.测试用例和测试数据、测试步骤分离
测试用例
在这里插入图片描述测试步骤
在这里插入图片描述

c.我编写的ui自动化框架,目前有那些不足

当然,该自动化测试框架,还有如下几个不足之处:
1.未添加足够的日志,方便以后的问题定位
2.未添加足够的异常处理,应对driver可能不稳定的情况
3.关于检测点的判断,目前的框架,只能检测字符串在某个特定的string里是否会出现。
检测点应该很多,如果真用此框架,后续这方面需要添加很多
4.缺少很多函数,比如判断元素是否存在,显示等待方面的内容,还有,需要根据项目特点,添加的一些函数
5.关于测试用列的优化,每个测试用列,都需要在上方添加很多装饰器,想把这些都抽出来,然后根据配置文件,添加到每个测试用列上
6.当然目前这个自动化框架还有很多不足,还是要根据具体的项目来进行进化

3.selenium

关于元素无法定位,抓紧收藏!Selenium无法定位元素的几种解决方案

4.pytest

6、Pytest之Fixture参数详解及使用

5.allure

关于allure的demo,可以参考,allure与pytest

6.验证

a.生成测试报告命令

pytest -v -s . --alluredir ./result --clean-alluredir
allure generate ./result/ -o ./reports/report --clean
allure open -h localhost -p 8080  ./reports/report/

b.查看测试报告
进入allure,查看测试报告
在这里插入图片描述查看fail的测试用列

在这里插入图片描述

在这里插入图片描述

五.环境准备

1.制作selenium的Chrome镜像

Ubuntu中安装Chrome和ChromeDriver,可以参考这边文档,ubuntu配置selenium
Ubuntu中安装allure,可以参考文档,Ubuntu安装allure
当前安装Chrome的版本

root@658a5d6f26c3:~/workspace# google-chrome --version
Google Chrome 107.0.5304.87 

Chromedriver最新的版本
在这里插入图片描述
使用dockerfile来制作selenium chrome镜像

(base) root@k8s-node1:~/cicd-prepar/selenium/chrome/test# cat dockerfile 
FROM ubuntu
MAINTAINER xusanduo 15618829160@163.com
#工作目录
WORKDIR /root/workspace
#先进行必要的更新,否则无法进行后续的安装操作
RUN  apt-get update \
  && apt-get -y install sudo dialog apt-utils \
  && sudo echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections \
  && sudo apt upgrade -y
#安装一些必要的软件,方便以后调式使用
RUN  sudo apt install nano tcpdump curl wget net-tools inetutils-ping git openssh-server unzip pip -y
#安装中文语言包,不然浏览器会出现乱码问题
RUN apt-get update && apt-get -y install ttf-wqy-microhei ttf-wqy-zenhei && apt-get clean
#安装Java,因为allure需要使用
RUN  sudo wget https://download.oracle.com/java/19/latest/jdk-19_linux-x64_bin.tar.gz \
  && sudo tar -zxvf jdk-19_linux-x64_bin.tar.gz \
  && sudo rm -rf jdk-19_linux-x64_bin.tar.gz
#安装Chrome浏览器,当前最新的版本是107
RUN  sudo wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \
#安装Chrome前,需要安装这些依赖,不然会报错
  && sudo apt install libasound2 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcairo2 libcups2 libdrm2 libgbm1 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libwayland-client0 libxcomposite1 libxdamage1 >
  && sudo dpkg -i google-chrome-stable_current_amd64.deb \
  && sudo rm -r google-chrome-stable_current_amd64.deb
#安装Chrome版本对应的Chromedriver
RUN sudo wget https://chromedriver.storage.googleapis.com/107.0.5304.62/chromedriver_linux64.zip \
  && sudo unzip chromedriver_linux64.zip \
  && sudo mv chromedriver /usr/bin/chromedriver \
  && sudo chown root:root /usr/bin/chromedriver \
  && sudo chmod +x /usr/bin/chromedriver \
  && sudo rm -r chromedriver_linux64.zip
#配置Java的环境变量
ENV JAVA_HOME=/root/workspace/jdk-19.0.1
ENV JRE_HOME=$JAVA_HOME/jre
ENV CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH
ENV PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH
#安装allure,当前最新的allure版本为2.19
RUN sudo wget https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/2.19.0/allure-commandline-2.19.0.tgz \
  && sudo tar -zxvf allure-commandline-2.19.0.tgz \
  && sudo rm -r allure-commandline-2.19.0.tgz \
  && sudo ln -s /root/workspace/allure-2.19.0/bin/allure /usr/bin/allure
#安装selenium,pyyaml,pytest,allure-pytest
RUN sudo pip install selenium pyyaml pytest allure-pytest
#配置ssh能够root登录
RUN sudo echo "PermitRootLogin yes" >> /etc/ssh/sshd_config \
  && sudo echo "service ssh restart" >> ~/.bashrc
#配置容器的root账号和密码
RUN  sudo echo root:123456 | chpasswd

(base) root@k8s-node1:~/cicd-prepar/selenium/chrome/test#

生成镜像和推送到仓库命令:

docker build . -t harbor.xusanduo.com/library/selenium-chrome-base:v1
docker push harbor.xusanduo.com/library/selenium-chrome-base:v1

a.build镜像遇到的问题

安装的时候Chrome的时候,会出现说很多依赖没有安装

Unpacking google-chrome-stable (107.0.5304.87-1) ...
dpkg: dependency problems prevent configuration of google-chrome-stable:
 google-chrome-stable depends on fonts-liberation; however:
  Package fonts-liberation is not installed.
 google-chrome-stable depends on libasound2 (>= 1.0.17); however:
  Package libasound2 is not installed.
 google-chrome-stable depends on libatk-bridge2.0-0 (>= 2.5.3); however:
  Package libatk-bridge2.0-0 is not installed.
 google-chrome-stable depends on libatk1.0-0 (>= 2.2.0); however:
  Package libatk1.0-0 is not installed.
 google-chrome-stable depends on libatspi2.0-0 (>= 2.9.90); however:
  Package libatspi2.0-0 is not installed.
 google-chrome-stable depends on libcairo2 (>= 1.6.0); however:
  Package libcairo2 is not installed.
 google-chrome-stable depends on libcups2 (>= 1.6.0); however:
  Package libcups2 is not installed.
 google-chrome-stable depends on libdrm2 (>= 2.4.60); however:
  Package libdrm2 is not installed.
 google-chrome-stable depends on libgbm1 (>= 8.1~0); however:
  Package libgbm1 is not installed.
 google-chrome-stable depends on libgtk-3-0 (>= 3.9.10) | libgtk-4-1; however:
  Package libgtk-3-0 is not installed.
  Package libgtk-4-1 is not installed.
 google-chrome-stable depends on libnspr4 (>= 2:4.9-2~); however:
  Package libnspr4 is not installed.
 google-chrome-stable depends on libnss3 (>= 2:3.26); however:
  Package libnss3 is not installed.
 google-chrome-stable depends on libpango-1.0-0 (>= 1.14.0); however:
  Package libpango-1.0-0 is not installed.
 google-chrome-stable depends on libwayland-client0 (>= 1.0.2); however:
  Package libwayland-client0 is not installed.
 google-chrome-stable depends on libxcomposite1 (>= 1:0.4.4-1); however:
  Package libxcomposite1 is not installed.
 google-chrome-stable depends on libxdamage1 (>= 1:1.1); however:
  Package libxdamage1 is not installed.
 google-chrome-stable depends on libxfixes3; however:
  Package libxfixes3 is not installed.
 google-chrome-stable depends on libxkbcommon0 (>= 0.4.1); however:
  Package libxkbcommon0 is not installed.
 google-chrome-stable depends on libxrandr2; however:
  Package libxrandr2 is not installed.
 google-chrome-stable depends on xdg-utils (>= 1.0.2); however:
  Package xdg-utils is not installed.

dpkg: error processing package google-chrome-stable (--install):
 dependency problems - leaving unconfigured
Errors were encountered while processing:
 google-chrome-stable
The command '/bin/sh -c sudo wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb   && sudo apt-get -f install   && sudo dpkg -i google-chrome-stable_current_amd64.deb' returned a non-zero code: 1

所以在dockerfile中添加了这句话

sudo apt install libasound2 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcairo2 libcups2 libdrm2 libgbm1 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libwayland-client0 libxcomposite1 libxdamage1 libxfixes3 libxkbcommon0 libxrandr2 xdg-utils fonts-liberation -y

b.docker容器中文乱码问题

在docker中因为没有中文字体,会出现中文乱码问题,
博客1:使用docker部署chrome无头浏览器并解决中文乱码,为pyppeteer提供运行环境
博客2:Docker中浏览器访问内网,并解决无中文字体问题
下面这篇,更实用
博客3:python dockerfile_菜鸟视角–用 Dockerfile 构建测试环境 (镜像包括 python chrome 浏览器 解决中文乱码问题)…
官方selenium镜像,解决中文乱码问题
博客4:docker启动selenium grid解决访问中文web问题

c.脚本在docker容器无法运行问题

使用命令运行创建容器,新创建的容器为effdb7c0809f

docker run -itd harbor.xusanduo.com/library/selenium-chrome-customize:v1

将本博客的第三章中的测试demo的工程拷贝到容器中,命令如下

docker cp /root/PycharmProjects/uitestdemo effdb7c0809f:/root/workspace

然后进入容器

docker exec -it effdb7c0809f bash

然后运行命令,启动pytest脚本,会出现报错

root@effdb7c0809f:~/uitestdemo# pytest -v -s .
=========================================================================================== test session starts ===========================================================================================
platform linux -- Python 3.8.10, pytest-7.2.0, pluggy-1.0.0 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /root/uitestdemo
plugins: allure-pytest-2.11.1
collected 11 items                                                                                                                                                                                        
cases/news/test_news.py::Test_news::test_click_one_hot_new[1] ERROR
cases/news/test_news.py::Test_news::test_click_one_hot_new[2] ERROR
cases/news/test_news.py::Test_news::test_click_one_hot_new[15] ERROR
cases/news/test_news.py::Test_news::test_click_one_hot_new[8] ERROR
cases/news/test_news.py::Test_news::test_click_one_hot_new[12] ERROR
cases/news/test_news.py::Test_news::test_click_one_local_new[4] ERROR
cases/news/test_news.py::Test_news::test_click_one_local_new[11] ERROR
cases/news/test_news.py::Test_news::test_click_one_local_new[9] ERROR
cases/news/test_news.py::Test_news::test_click_one_local_new[13] ERROR
cases/news/test_news.py::Test_news::test_click_one_local_new[2] ERROR

================================================================================================= ERRORS ==================================================================================================
__________________________________________________________________________ ERROR at setup of Test_news.test_click_one_hot_new[1] __________________________________________________________________________

self = <class 'news.test_news.Test_news'>

    def setup_class(self):
        with allure.step(page_test_data['Test_news']['setup_steps'][0]):
>           self.app = App().start()

cases/news/test_news.py:13: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
pages/base/app.py:25: in start
    self.allure_screenshot('main_page', self.config['screenshots_path'] + 'main_page.PNG')
pages/base/base.py:26: in allure_screenshot
    self.screenshot(file_path)
pages/base/base.py:22: in screenshot
    self._driver.get_screenshot_as_file(name)
/usr/local/lib/python3.8/dist-packages/selenium/webdriver/remote/webdriver.py:929: in get_screenshot_as_file
    png = self.get_screenshot_as_png()
/usr/local/lib/python3.8/dist-packages/selenium/webdriver/remote/webdriver.py:965: in get_screenshot_as_png
    return b64decode(self.get_screenshot_as_base64().encode("ascii"))
/usr/local/lib/python3.8/dist-packages/selenium/webdriver/remote/webdriver.py:977: in get_screenshot_as_base64
    return self.execute(Command.SCREENSHOT)["value"]
/usr/local/lib/python3.8/dist-packages/selenium/webdriver/remote/webdriver.py:444: in execute
    self.error_handler.check_response(response)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <selenium.webdriver.remote.errorhandler.ErrorHandler object at 0x7f4c170c78e0>
response = {'status': 500, 'value': '{"value":{"error":"unknown error","message":"unknown error: unhandled inspector error: {\\"c...\\n#18 0x5618e46c58d2 \\u003Cunknown>\\n#19 0x5618e46df99f \\u003Cunknown>\\n#20 0x7fb46832b609 \\u003Cunknown>\\n"}}'}
.................省略........................
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
========================================================================================= short test summary info =========================================================================================
ERROR cases/news/test_news.py::Test_news::test_click_one_hot_new[1] - selenium.common.exceptions.WebDriverException: Message: unknown error: unhandled inspector error: {"code":-32000,"message":"Unable to capture screenshot"}
ERROR cases/news/test_news.py::Test_news::test_click_one_hot_new[2] - selenium.common.exceptions.WebDriverException: Message: unknown error: unhandled inspector error: {"code":-32000,"message":"Unable to capture screenshot"}
ERROR cases/news/test_news.py::Test_news::test_click_one_hot_new[15] - selenium.common.exceptions.WebDriverException: Message: unknown error: unhandled inspector error: {"code":-32000,"message":"Unable to capture screenshot"}
ERROR cases/news/test_news.py::Test_news::test_click_one_hot_new[8] - selenium.common.exceptions.WebDriverException: Message: unknown error: unhandled inspector error: {"code":-32000,"message":"Unable to capture screenshot"}
ERROR cases/news/test_news.py::Test_news::test_click_one_hot_new[12] - selenium.common.exceptions.WebDriverException: Message: unknown error: unhandled inspector error: {"code":-32000,"message":"Unable to capture screenshot"}
ERROR cases/news/test_news.py::Test_news::test_click_one_local_new[4] - selenium.common.exceptions.WebDriverException: Message: unknown error: unhandled inspector error: {"code":-32000,"message":"Unable to capture screenshot"}
ERROR cases/news/test_news.py::Test_news::test_click_one_local_new[11] - selenium.common.exceptions.WebDriverException: Message: unknown error: unhandled inspector error: {"code":-32000,"message":"Unable to capture screenshot"}
ERROR cases/news/test_news.py::Test_news::test_click_one_local_new[9] - selenium.common.exceptions.WebDriverException: Message: unknown error: unhandled inspector error: {"code":-32000,"message":"Unable to capture screenshot"}
ERROR cases/news/test_news.py::Test_news::test_click_one_local_new[13] - selenium.common.exceptions.WebDriverException: Message: unknown error: unhandled inspector error: {"code":-32000,"message":"Unable to capture screenshot"}
ERROR cases/news/test_news.py::Test_news::test_click_one_local_new[2] - selenium.common.exceptions.WebDriverException: Message: unknown error: unhandled inspector error: {"code":-32000,"message":"Unable to capture screenshot"}

出现上述问题后,看起来是比较诡异的,因为本地调试没有问题,在容器中会出现问题。
而且,给出的报错信息,貌似是跟内存没一毛钱关系,实际上是因为容器中的内存不足导致,因为docker容器的默认内存是64M,root-cause知道了,解决方案,就是加内存,创建容器的命令,更改为如下:

docker run -itd --shm-size 2g harbor.xusanduo.com/library/selenium-chrome-customize:v1

2.制作selenium的Firefox镜像

firefox浏览器的下载地址:http://firefox.com.cn/download/
firefox driver的下载地址:https://github.com/mozilla/geckodriver/releases

制作selenium的Firefox镜像的dockerfile如下,特别说明,Firefox浏览器,我是手动下载,然后拷贝到镜像中去的,
如果使用此dockerfile,需要提前下载好Firefox浏览器到对应的目录下

root@k8s-node1:~/cicd-prepar/selenium/firefox# ls
dockerfile  Firefox-latest-x86_64.tar.bz2
root@k8s-node1:~/cicd-prepar/selenium/firefox# cat dockerfile 
FROM ubuntu
MAINTAINER xusanduo 15618829160@163.com
#工作目录
WORKDIR /root/workspace
#先进行必要的更新,否则无法进行后续的安装操作
RUN  apt-get update \
  && apt-get -y install sudo dialog apt-utils \
  && sudo echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections \
  && sudo apt upgrade -y
#安装一些必要的软件,方便以后调式使用
RUN  sudo apt install nano tcpdump curl wget net-tools inetutils-ping git openssh-server unzip pip -y
#安装中文语言包,不然浏览器会出现乱码问题
RUN apt-get update && apt-get -y install ttf-wqy-microhei ttf-wqy-zenhei && apt-get clean
#安装Java,因为allure需要使用
RUN  sudo wget https://download.oracle.com/java/19/latest/jdk-19_linux-x64_bin.tar.gz \
  && sudo tar -zxvf jdk-19_linux-x64_bin.tar.gz \
  && sudo rm -rf jdk-19_linux-x64_bin.tar.gz
#配置Java的环境变量
ENV JAVA_HOME=/root/workspace/jdk-19.0.1
ENV JRE_HOME=$JAVA_HOME/jre
ENV CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH
ENV PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH
#安装allure,当前最新的allure版本为2.19
RUN sudo wget https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/2.19.0/allure-commandline-2.19.0.tgz \
  && sudo tar -zxvf allure-commandline-2.19.0.tgz \
  && sudo rm -r allure-commandline-2.19.0.tgz \
  && sudo ln -s /root/workspace/allure-2.19.0/bin/allure /usr/bin/allure
#拷贝firefox浏览器
COPY . .
#安装Firefox浏览器的相关依赖,不然会出现启动报错的情况
RUN sudo apt install libgtk-3-0 libasound2 libdbus-glib-1-dev libx11-xcb-dev -y
#安装firefox浏览器
RUN sudo tar jxvf Firefox-latest-x86_64.tar.bz2 \
  && sudo chown -R root:root /root/workspace/firefox \
  && sudo ln -s /root/workspace/firefox/firefox /usr/bin/firefox \
  && sudo chmod +x /usr/bin/firefox \
  && sudo rm Firefox-latest-x86_64.tar.bz2
#安装firefox的driver
RUN sudo wget https://github.com/mozilla/geckodriver/releases/download/v0.32.0/geckodriver-v0.32.0-linux64.tar.gz \
  && sudo tar -zxvf geckodriver-v0.32.0-linux64.tar.gz \
  && sudo mv geckodriver /usr/local/share/ \
  && sudo ln -s /usr/local/share/geckodriver /usr/local/bin/geckodriver \
  && sudo ln -s /usr/local/share/geckodriver /usr/bin/geckodriver \
  && sudo rm geckodriver-v0.32.0-linux64.tar.gz

#安装selenium,pyyaml,pytest,allure-pytest
RUN sudo pip install selenium pyyaml pytest allure-pytest
#配置ssh能够root登录
RUN sudo echo "PermitRootLogin yes" >> /etc/ssh/sshd_config \
  && sudo echo "service ssh restart" >> ~/.bashrc
#配置容器的root账号和密码
RUN  sudo echo root:123456 | chpasswd
root@k8s-node1:~/cicd-prepar/selenium/firefox# 

生成镜像和推送到仓库命令:

docker build . -t harbor.xusanduo.com/library/selenium-firefox-base:v1
docker push harbor.xusanduo.com/library/selenium-firefox-base:v1

关于显示,图像化界面,可以参考博客,图像化界面 显示firefox浏览器
关于Firefox图形化显示,暂时没做,后续有时间再倒腾

3.制作selenium的Edge镜像

ubuntu安装edge浏览器,可以参考地址:如何在 Ubuntu 中安装 Microsoft Edge 浏览器
edgedriver的地址:edgedriver的官方地址
获取edge浏览器有那些稳定版本,可以使用命令(需要现在apt里面配置好微软的仓库地址)

root@6f7012421bf8:~/workspace# apt-cache madison microsoft-edge-stable
microsoft-edge-stable | 107.0.1418.42-1 | https://packages.microsoft.com/repos/edge stable/main amd64 Packages
microsoft-edge-stable | 107.0.1418.35-1 | https://packages.microsoft.com/repos/edge stable/main amd64 Packages
............后面还有很多..............

制作selenium的Edge镜像的dockerfile如下,特别说明,edge浏览器有的版本,不一定有linux版本的driver,请查看清楚

root@k8s-node1:~/cicd-prepar/selenium/edge# cat dockerfile 
FROM ubuntu
MAINTAINER xusanduo 15618829160@163.com
#工作目录
WORKDIR /root/workspace
#先进行必要的更新,否则无法进行后续的安装操作
RUN  apt-get update \
  && apt-get -y install sudo dialog apt-utils \
  && sudo echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections \
  && sudo apt upgrade -y
#安装一些必要的软件,方便以后调式使用
RUN  sudo apt install nano tcpdump curl wget net-tools inetutils-ping git openssh-server unzip pip -y
#安装中文语言包,不然浏览器会出现乱码问题
RUN apt-get update && apt-get -y install ttf-wqy-microhei ttf-wqy-zenhei && apt-get clean
#安装Java,因为allure需要使用
RUN  sudo wget https://download.oracle.com/java/19/latest/jdk-19_linux-x64_bin.tar.gz \
  && sudo tar -zxvf jdk-19_linux-x64_bin.tar.gz \
  && sudo rm -rf jdk-19_linux-x64_bin.tar.gz
#配置Java的环境变量
ENV JAVA_HOME=/root/workspace/jdk-19.0.1
ENV JRE_HOME=$JAVA_HOME/jre
ENV CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH
ENV PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH
#安装allure,当前最新的allure版本为2.19
RUN sudo wget https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/2.19.0/allure-commandline-2.19.0.tgz \
  && sudo tar -zxvf allure-commandline-2.19.0.tgz \
  && sudo rm -r allure-commandline-2.19.0.tgz \
  && sudo ln -s /root/workspace/allure-2.19.0/bin/allure /usr/bin/allure
#安装edge浏览器,目前最新的edge浏览器稳定版本为:107.0.1418.42
RUN sudo apt install apt-transport-https ca-certificates curl software-properties-common wget -y \
  && sudo wget -O- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /usr/share/keyrings/microsoft-edge.gpg \
  && sudo touch /etc/apt/sources.list.d/microsoft-edge.list \
  && sudo echo 'deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-edge.gpg] https://packages.microsoft.com/repos/edge stable main' >> /etc/apt/sources.list.d/microsoft-edge.list \
  && sudo apt update && apt-get upgrade -y \
  && sudo apt install microsoft-edge-stable -y \
  && sudo apt clean 
#安装edge的driver,下载浏览器版本对应的driver版本
RUN sudo wget https://msedgedriver.azureedge.net/107.0.1418.42/edgedriver_linux64.zip \
  && sudo unzip edgedriver_linux64.zip \
  && sudo mv msedgedriver /usr/bin/msedgedriver \
  && sudo chown root:root /usr/bin/msedgedriver \
  && sudo chmod +x /usr/bin/msedgedriver \
  && sudo rm -r edgedriver_linux64.zip

#安装selenium,pyyaml,pytest,allure-pytest
RUN sudo pip install selenium pyyaml pytest allure-pytest
#配置ssh能够root登录
RUN sudo echo "PermitRootLogin yes" >> /etc/ssh/sshd_config \
  && sudo echo "service ssh restart" >> ~/.bashrc
#配置容器的root账号和密码
RUN  sudo echo root:123456 | chpasswd
root@k8s-node1:~/cicd-prepar/selenium/edge# 

生成镜像并且推送到仓库命令:

docker build . -t harbor.xusanduo.com/library/selenium-edge-base:v1
docker push harbor.xusanduo.com/library/selenium-edge-base:v1

六.实验

为了方便,本次的jenkins node节点的镜像和部署,均是在上一篇博客的基础上进行的。让这个jenkins node既可以部署spring boot应用,也可以部署测试脚本

1.制作jenkins node节点的镜像

关于jenkins node的镜像制作,参考的是上一篇博客关于node节点的制作,坐井观天说Devops–3–开发CICD之springboot分布式持续集成持续交付,我这次只是在原来的基础上,增加了allure,其他均保持不变。
子节点的的dockerfile内容如下:

root@k8s-node1:~/cicd-prepar/springbootapp/jenkinsnode# cat dockerfile 
FROM ubuntu
MAINTAINER xusanduo 15618829160@163.com
RUN  apt-get update \
  && apt-get -y install sudo dialog apt-utils \
  && sudo echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections \
  && sudo apt upgrade -y \
  && sudo apt install nano tcpdump curl wget net-tools inetutils-ping git openssh-server -y \
  && sudo wget https://download.oracle.com/java/19/latest/jdk-19_linux-x64_bin.tar.gz \
  && sudo tar -xzvf jdk-19_linux-x64_bin.tar.gz  \
  && sudo apt install maven -y \
  && sudo echo 'export JAVA_HOME=/jdk-19.0.1' >> /etc/profile \
  && sudo echo 'export JRE_HOME=$JAVA_HOME/jre' >> /etc/profile \
  && sudo echo 'export CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH' >> /etc/profile \
  && sudo echo 'export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH' >> /etc/profile \
  && sudo echo "PermitRootLogin yes" >> /etc/ssh/sshd_config \
  && sudo echo "source /etc/profile" >> ~/.bashrc \
  && sudo echo "service ssh restart" >> ~/.bashrc
#安装allure,当前最新的allure版本为2.19
RUN sudo wget https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/2.19.0/allure-commandline-2.19.0.tgz \
  && sudo tar -zxvf allure-commandline-2.19.0.tgz \
  && sudo rm -r allure-commandline-2.19.0.tgz \
  && sudo ln -s /allure-2.19.0/bin/allure /usr/bin/allure
RUN sudo echo root:123456 | chpasswd
RUN git config --global user.name xuerhe \
 && git config --global user.email wanliang0526@qq.com
COPY start.sh /root/

其中的sleep.sh脚本是为了让容器能够持续运行的,脚本如下:

(base) root@k8s-node1:~/temp/jenkinsnode# cat start.sh 
#!/bin/bash
#表示从configmap中挂载的文件
keys_private_key_path="/keys/id_rsa"
keys_public_key_path="/keys/id_rsa.pub"

ssh_path="/root/.ssh"
#表示ssh的秘钥写入的文件路径
ssh_private_key_path="/root/.ssh/id_rsa"
ssh_public_key_path="/root/.ssh/id_rsa.pub"

#start ssh service
start_ssh_server(){
  service ssh restart
}

#如果直接将configmap中的公钥私钥,挂载到容器中,这样.ssh文件夹是onlyread状态。
#该方法,就是configmap中的ssh的公钥私钥转化到容器中去。因为不想通过nfs挂载等方式,直接就通过这种方式
#来将ssh的公钥和私钥整合到容器
copy_sshkeys_from_configmap(){
  while true
    do sleep 3
    if [ -f $keys_private_key_path ] && [ ! -d $ssh_path ]
    then
      mkdir /root/.ssh
      cat $keys_private_key_path >> $ssh_private_key_path
      cat $keys_public_key_path >> $ssh_public_key_path
      chmod 600 $ssh_path
      chmod 600 $ssh_private_key_path
      chmod 644 $ssh_public_key_path
    fi
    done
}
start_ssh_server
copy_sshkeys_from_configmap
(base) root@k8s-node1:~/temp/jenkinsnode# 

生成镜像和push到harbor仓库的命令如下:

docker build . -t harbor.xusanduo.com/library/jenkinsnode:v2
docker push harbor.xusanduo.com/library/jenkinsnode:v2

2.部署jenkins node镜像

因为浏览器容器在测试完成之后,会自动关闭掉,所以浏览器容器生成的测试报告和测试数据,和jenkins node节点,要共享一个文件存储卷,这样浏览器容器和jenkins node的数据能够共享。
所以,我们需要制作1个pvc,专门用来存放测试数据和测试报告。

a.准备pvc

准备pvc用来存放测试数据,特别说明,另外3个pvc保持不变,那是springboot专用

root@k8s-master1:~/k8s/jenkinsnode# cat pvc-for-test-data.yaml 
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-for-test-data
  namespace: devops
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: nfs-storage
  resources:
    requests:
      storage: 4Gi
root@k8s-master1:~/k8s/jenkinsnode# kubectl get pvc -n devops 
NAME                STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc-for-node1       Bound    pvc-1c0ef4d6-c467-4861-aeba-bd82f662859a   4Gi        RWX            nfs-storage    28d
pvc-for-node2       Bound    pvc-c1eb771c-0a90-412b-9c60-952bfc7f55d9   4Gi        RWX            nfs-storage    28d
pvc-for-node3       Bound    pvc-1adafa8c-b4a3-4434-9262-6666b083a246   4Gi        RWX            nfs-storage    28d
pvc-for-test-data   Bound    pvc-cd6011c6-0e2b-4751-80d7-4132cdeb9171   4Gi        RWX            nfs-storage    46s
root@k8s-master1:~/k8s/jenkinsnode# kubectl get pv -n devops 
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                      STORAGECLASS   REASON   AGE
pvc-1adafa8c-b4a3-4434-9262-6666b083a246   4Gi        RWX            Retain           Bound    devops/pvc-for-node3       nfs-storage             28d
pvc-1c0ef4d6-c467-4861-aeba-bd82f662859a   4Gi        RWX            Retain           Bound    devops/pvc-for-node1       nfs-storage             28d
pvc-c1eb771c-0a90-412b-9c60-952bfc7f55d9   4Gi        RWX            Retain           Bound    devops/pvc-for-node2       nfs-storage             28d
pvc-cd6011c6-0e2b-4751-80d7-4132cdeb9171   4Gi        RWX            Retain           Bound    devops/pvc-for-test-data   nfs-storage             52s
root@k8s-master1:~/k8s/jenkinsnode# 

b.k8s部署jenkins node

本次部署的jenkins node,为了简单,是在上一篇博客的基础上,仅仅是在jenkins-nodes-ubuntu-docker1节点上,将新增加的pvc给挂载上去而已,然后镜像的版本选择v2

root@k8s-master1:~/k8s/jenkinsnode# cat jenkinsnode-deploy.yaml 
apiVersion: v1
kind: Service
metadata:
  name: jenkins-node1
  namespace: devops
  labels:
    app: jenkins-node1
spec:
  type: ClusterIP
  ports:
  - name: ssh
    port: 22          #服务端口
    targetPort: 22
  selector:
    app: jenkins-node1
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jenkins-node1
  namespace: devops
  labels:
    app: jenkins-node1
spec:
  selector:
    matchLabels:
      app: jenkins-node1
  replicas: 1
  template:
    metadata:
      labels:
        app: jenkins-node1
    spec:
      nodeName: k8s-node2
      hostname: jenkins-node1
      hostAliases:
      - ip: 192.168.100.199
        hostnames:
        - "gitlab.xusanduo.com"
      - ip: 192.168.100.200
        hostnames:
        - "harbor.xusanduo.com"
      containers:
      - name: jenkins-node1
        image: harbor.xusanduo.com/library/jenkinsnode:v2
        command: ["/bin/sh", "-c", "/root/start.sh"]
        securityContext:
          runAsUser: 0       #设置以ROOT用户运行容器
          privileged: true   #拥有特权
        ports:
        - name: ssh
          containerPort: 22
        volumeMounts:                        #设置要挂在的目录
        - name: docker-sock
          mountPath: /var/run/docker.sock
        - name: docker-client
          mountPath: /usr/bin/docker
        - name: jenkinsnodeworkerspace
          mountPath: /home
        - name: id-rsa
          mountPath: /keys
        - name: kubectl-client
          mountPath: /usr/bin/kubectl
        - name: kubectl-config
          mountPath: /root/.kube/config
        - name: deploy
          mountPath: /deploy
        - name: uitestdemotestdata       #特别说明1:本次新增的pv挂载到本目录下
          mountPath: /uitestdemotestdata
      volumes:
      - name: docker-sock
        hostPath:
          path: /var/run/docker.sock
      - name: docker-client
        hostPath:
          path: /usr/bin/docker
      - name: jenkinsnodeworkerspace
        persistentVolumeClaim:
          claimName: pvc-for-node1
      - name: id-rsa
        configMap:
          name: jenkins-node-connect-gitlab
      - name: kubectl-client
        hostPath:
          path: /usr/bin/kubectl
      - name: kubectl-config
        hostPath:
          path: /root/.kube/config
      - name: deploy
        configMap:
          name: deploy-app
      - name: uitestdemotestdata   #特别说明1:本次新增的pvc
        persistentVolumeClaim:
          claimName: pvc-for-test-data
---
另外的两个节点保持不变
........................

3.UI自动化框架的代码更新(v2版本)

相对于博客的第3节中的ui自动化框架中的代码,只有两处更新,一是更新config.yaml文件,方便jenkins能够动态的选择url和浏览器,二是新增了一个start.sh脚本文件

a.config.yaml文件更改

root@k8s-node1:/home/workspace/uitestdemo/config# cat config.yaml
baseUrl: temp_test_target_url  #特别说明:v2版本更新的地方,没有说明的地方,均保持不变
browser:
  #browser only support Chrome, Firefox, Edge
  type: temp_browser_type     #特别说明:v2版本更新的地方,没有说明的地方,均保持不变
  #option only support ChromeOption, FirefoxOption, EdgeOption
  options: temp_browser_options_type    #特别说明:v2版本更新的地方,没有说明的地方,均保持不变
  optionsContent:
    - '--headless'
    - '--disable-gpu'
    - '--no-sandbox'
    - '--ignore-certificate-errors'
    - '--window-size=1920,1080'

log_config:
  #log config file path
  log_config_file_path: './config/logging.conf'
  #log output type,consolelog or filelog
  log_output_type: 'filelog'

page_data_path:
  main: './data/main.yaml'
  news: './data/news.yaml'

page_test_data_path:
  test_news: './data/test_news.yaml'

screenshots_path: './reports/screenshots/'root@k8s-node1:/home/workspace/uitestdemo/config# 

b.新增start.sh启动脚本文件

还有一处更改,就是相对于v1版本,新增了一个start.sh脚本文件,本文件的作用,主要就是在共享文件夹中,创建本次构建相关的文件夹,启动测试脚本

root@k8s-node1:/home/workspace/uitestdemo# cat start.sh
#!/bin/bash

cd "/root/workspace/uitestdemo"
#选择执行测试用例的级别
#caseLevel="highs"
caseLevel='temp_caseLevel'
#获取当前pod的名称
podname=$(echo $POD_NAME|awk -F "-" '{print $8}')
#podname='podabc'
#根据当前节点名称、pod名称以及UUID,拼接一个名称,用作后续pod容器名称
uuid=$(cat /proc/sys/kernel/random/uuid|md5sum|cut -c 1-9)
#podtestdatafoldername="k8s-node1-"${podname}'-'$uuid
podtestdatafoldername=${NODE_NAME}"-"${podname}'-'$uuid

#所有本次跑的pod,都会将测试数据放在该目录下
#testdatafoldername='2022-11-22-11-39-29'
testdatafoldername='temp_build_id'

#这个是共享文件夹的路径,所有测试数据,都会放在这个目录下
testpath='/uitestdemotestdata/'

projectName='temp_job_name'

#本次跑的allure测试数据,都会在该目录下
podtestresultpath=${testpath}${projectName}'/'${testdatafoldername}'/'${podtestdatafoldername}'/result'

#本次构建,所有的pod的allure测试数据,均会放在该目录下
testprojectresultpath=${testpath}${projectName}'/'${testdatafoldername}'/result/'
#本次构建,allure的测试报告
testprojectreportpath=${testpath}${projectName}'/'${testdatafoldername}'/report/'

#创建相关的文件
echo $podtestresultpath
mkdir -p $podtestresultpath
mkdir -p $testprojectresultpath
mkdir -p $testprojectreportpath

#当前脚本的路径
currentPath=$(pwd)
#测试脚本的路径
casesPath=$currentPath"/cases/"

#替换cases文件夹下所有的测试类的名称,在测试类的名称后面加上pod的名称,在测试用例加入pod名称,主要目的是为后面将多个pod
#的测试数据,能够整合成一份测试报告
replaceCaseClassname(){
   #获取cases路径下的所有测试用例中的文件,并且文件以test_*.py格式
   files=$(find $casesPath -type f -name "test_*.py")
   for file in $files;do
      #获取每个test_*.py文件中的类名,并且类名是以Test_开头
      classNames=$(cat $file |awk '/class /{split($2,arr,":");print arr[1]}')
      for className in $classNames;do
         targetClassname=${className}"_"$podname
         targetClassnameLength=$(expr length $targetClassname)
         echo "replaceCaseClassname function---------------targetClassname---------"$targetClassname
         #podname的长度是5,加上测试类名是以Test_开头,长度也为5,加起来为10
         if [ $targetClassnameLength -gt 10 ] && [ ${targetClassname: 0: 5} == "Test_" ];then
           tempClassName=${className}":"
           tempTargetClassname=${targetClassname}":"
           sed -i s#$tempClassName#$tempTargetClassname#g $file
         fi
      done
      echo "replaceCaseClassname function----the source file "$file" and the point classNames is "$classNames
   done
}

runTestcase(){
   echo "runTestcase--------current caselevel is "$caseLevel
   if [ $caseLevel == "highs" ] || [ $caseLevel == "lows" ];then
      pytest -v -m $caseLevel -s . --alluredir $podtestresultpath --clean-alluredir
   elif [ $caseLevel == "all" ];then
      pytest -v -s . --alluredir $podtestresultpath --clean-alluredir
   else
      echo "runTestcase funciton-----------the caselevel param is error,and the param is "$caseLevel
   fi
   cp -r $podtestresultpath'/.' $testprojectresultpath
}
run(){
   replaceCaseClassname
   runTestcase
}
run
exit 0
root@k8s-node1:/home/workspace/uitestdemo# 

特别说明:
在pod容器中,生成的测试结果数据的目录结构,应该如下

root@k8s-node1:/uitestdemotestdata# tree
.
└── temp_job_name
    └── temp_build_id
        ├── k8s-node1-podabc-04ace1a65
        │   └── result
        ├── k8s-node2-podabc-dders1a37
        │   └── result
        ├── report
        └── result

8 directories, 0 files
root@k8s-node1:/uitestdemotestdata# 

4.selenium浏览器部署相关

a.部署的模板文件

关于k8s的job使用说明,可以参考官方的文档

root@k8s-node1:/deploy# cat job_uitestdemo_browser_template.yaml
apiVersion: batch/v1
kind: Job
metadata:
  namespace: devops
  name: temp_JobName
spec:
  template:
    spec:
      nodeName: temp_k8s-node
      containers:
      - name: temp_PodLableName
        image: temp_ImageName
        command: ['/bin/sh', '-c','/root/workspace/uitestdemo/start.sh']
        securityContext:
          runAsUser: 0
          privileged: true
        resources:
          limits:
            memory: "1Gi"
          requests:
            memory: "1Gi"
        volumeMounts:
          - name: dshm
            mountPath: /dev/shm
          - name: uitestdemotestdata
            mountPath: /uitestdemotestdata
        env:
          - name: NODE_NAME
            valueFrom:
              fieldRef:
                fieldPath: spec.nodeName
          - name: POD_NAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
      volumes:
         - name: dshm
           emptyDir:
             medium: Memory
         - name: uitestdemotestdata
           persistentVolumeClaim:
             claimName: pvc-for-test-data
      restartPolicy: Never
  parallelism: temp_PodNumbers
  completions: temp_PodNumbers
  backoffLimit: 2
root@k8s-node1:/deploy# 

b.jenkins节点上的脚本文件

该脚本比较关键,处于一种控制中心的作用,主要有如下几种作用:
1.用来接受jenkins的构建参数,比如脚本的版本,部署动作,测试URL,浏览器类型,浏览器个数,测试用例级别等
2.处理下载好的testproject工程,将1中接受的参数,动态替换测试脚本项目中的config.yaml和start.sh文件中。替换完成后,将testproject打包成tar文件,并且制作成selenium的浏览器镜像,然后推送到harbor上去
3.处理selenium浏览器部署的模板文件job_uitestdemo_browser_template.yaml,动态生成根据用户选择,适配出的浏览器的selenium job,其中镜像文件,就是2中的新制作的镜像
脚本如下:

root@k8s-master1:~/k8s/jenkinsnode# cat deploy_uitestdemo.sh 
#!/bin/bash
#harbor仓库的账号密码
Username="admin"
Password="123456"
#harbor仓库的url地址
HaborUrl="http://harbor.xusanduo.com"
HaborWeb="harbor.xusanduo.com"
#默认的项目路径
DefaultProjectName="library"


#项目代码的版本
#codeversion=$codeVersion
#codeversion="v1"
codeversion=$1
#用户选择参数,部署or回滚or终止
#deploy_or_rollback=$deploy_action
#deploy_action="deploy"
deploy_action=$2
#测试的url
#test_target_url="https://www.baidu.com"
test_target_url=$3
#浏览器的类型
#browser_type="chrome"
browser_type=$4
#浏览器的个数
#browser_numbers=1
browser_numbers=$5
#测试用例的测试级别
#case_level="highs"
case_level=$6
#构建job的id
#build_id="build_id"
build_id=$7
#项目的名称
#jobName="uitestdemo"
jobName=$8
#workspace=$WORKSPACE
#workspace="/home/workspace/uitestdemo"
workspace=$9

#获取当前时间
currentTime=$(date -d today +"%Y-%m-%d-%H-%M-%S")

#测试脚本工程的配置文件路径
test_config_path=$workspace"/config/config.yaml"
#测试脚本工程的启动脚本文件路径
test_start_path=$workspace"/start.sh"
#默认的镜像名称
DefaultImageName="selenium-"${browser_type}"-base"
#在harbor中,新生成的镜像名称
image_name_in_harbor="selenium-"${browser_type}"-customize"
#image_name_in_harbor="demo1"
#集群中的configmap映射到pod容器的路径
jobFileFromConfigmap="/deploy/job_uitestdemo_browser_template.yaml"

#脚本的项目路径
projectPath="/root/${jobName}"
#生成的模板文件路径
deployFile="$projectPath/deploy.yaml"
#测试脚本的开始的shell路径
startTestScript=$workspace"/start.sh"

#登录harbor.xusanduo.com网站的账号密码的配置文件路径
dockerConfigPath="/root/.docker"
#从configmap中,获取登录harbor.xusanduo.com网站的账号密码的配置文件
dockerConfigMapPath="/deploy/config.json"

#测试脚本的压缩包名称
testScriptsTarName=${jobName}".tar"

#部署时,应用默认的设置
default_node="k8s-node1"

#需要部署或者回滚的版本镜像名称
project_image_base_url=${HaborWeb}/${DefaultProjectName}/${DefaultImageName}
imageName=${HaborWeb}/${DefaultProjectName}/${image_name_in_harbor}:${codeversion}
#判断镜像的版本,是否在harbor仓库中存在
imageIsOrNotExistHarbor(){
   projectName="$1"
   imageName="$2"
   imageVersion="$3"
   urlResult=$(curl -s -u ${Username}:${Password}  -X GET -H "Content-Type: application/json" ${HaborUrl}/api/v2.0/projects/${projectName}/repositories/${imageName}/artifacts/${imageVersion}/tags)
   notFoundStr="NOT_FOUND"
   result=$(echo $urlResult | grep "${notFoundStr}")
   if [[ "$result" != "" ]]
   then
#      echo "imageIsOrNotExistHarbor------Image is not exsit in harbor,need to build a new image"
      echo 1
   else
#      echo "imageIsOrNotExistHarbor------Image is exsit in harbor,not need to build a new image"
      echo 0
   fi
}

#将测试脚本进行压缩
tarTestScriptsProject(){
   cd $workspace
   cd ..
   echo $testScriptsTarName $jobName
   tar -cvf $testScriptsTarName $jobName
   mv $testScriptsTarName $projectPath
}
#制作和push镜像到harbor
buildAndPushImage(){
   #创建测试脚本为tar压缩文件
   tarTestScriptsProject
   sudo mkdir -p $projectPath
   sudo rm -rf $projectPath/*
   #将构建的springboot应用的jar,打进docker镜像中去
   echo "buildAndPushImage-------It is building image"
   echo "FROM ${project_image_base_url}:v1"  >> $projectPath/dockerfile
   echo "COPY . ."  >> $projectPath/dockerfile
   echo "RUN sudo tar -xvf ${testScriptsTarName} && rm ${testScriptsTarName} dockerfile deploy_uitestdemo.sh" >> $projectPath/dockerfile
   cd $projectPath
   echo "buildAndPushImage-------Finish to build image"
   sudo docker build . -t $imageName
   sudo docker push $imageName
}
#将最新版本的应用部署到k8s集群中去
deploy(){
   #判断是否需要build镜像
   echo "deploy-------start deploy"
   result=$(imageIsOrNotExistHarbor $DefaultProjectName $image_name_in_harbor $codeversion)
   if test $result -eq 1;then
      insertJenkinsParamsToTestScripts
      echo "deploy-------The image is not exsit in harbor, need build"
      buildAndPushImage
      generateDeployYamlFile
      cd $projectPath
      kubectl apply -f $deployFile
      waitJobComplete
      echo "deploy-------deploy finished"
   elif test $result -eq 0;then
      insertJenkinsParamsToTestScripts
      echo "deploy-------The image is exsit in harbor, not need build"
      generateDeployYamlFile
      cd $projectPath
      kubectl apply -f $deployFile
      waitJobComplete
      echo "deploy-------deploy finished"
   else
      echo "deploy-------imageIsOrNotExistHarbor function is error, please check"
   fi
}

#将版本回滚到指定版本,并且部署到k8s集群中去
rollback(){
   insertJenkinsParamsToTestScripts
   #生成部署文件
   echo "rollback-----start rollback"
   generateDeployYamlFile
   cd $projectPath
   kubectl apply -f $deployFile
   waitJobComplete
}
#此功能暂时未实现,请忽略
stop(){
   #生成部署文件
   echo "stop-----stop app deploy"
   generateDeployYamlFile
   cd $projectPath
   #kubectl delete -f $deployFile
}

#检查job是否已经完成,如果完成返回0,如果没有完成返回1
checkjobcomlete(){
   jobname="$1"
   result=&(kubectl get jobs.batch -n devops |awk -v jobname="$jobname" '{split($2,arr,"/");if ($1==jobname && arr[1]==arr[2]) print "0"; if ($1==jobname && arr[1]<arr[2]) print "1"}')
   return $result
}

#等待job完成
waitJobComplete(){
   JobName=${jobName}"-"${currentTime}
   while true;do
     result=$(checkjobcomlete $JobName)
     if [[ $result == '1' ]];then
       echo "startJob function----------job not complete, please wait"
       sleep 10
     elif [[ $result == '0' ]];then
       echo "startJob function----------job complete!!!"
       break
     else
       echo "startJob function----------job not exsit!!!please check"
       break
     fi
   done
}

#获取部署模板文件
getDeployTemplateFile(){
   if [ -f $deployFile ]
   then
     echo "" > $deployFile
   fi
   cat $jobFileFromConfigmap >> $deployFile
}

#将jenkins的参数传入到测试脚本中去
insertJenkinsParamsToTestScripts(){
   #处理测试脚本工程中的start.sh文件,主要传入执行的测试用例级别和build_id,build_id主要用来存放测试结果数据
   sed -i s#temp_caseLevel#${case_level}#g $test_start_path
   sed -i s#temp_build_id#${build_id}#g $test_start_path
   sed -i s#temp_job_name#${jobName}#g $test_start_path
   #处理测试脚本的配置文件
   sed -i s#temp_test_target_url#${test_target_url}#g $test_config_path
   if [[ "$browser_type" == "chrome" ]]
   then
      sed -i s#temp_browser_type#Chrome#g $test_config_path
      sed -i s#temp_browser_options_type#ChromeOption#g $test_config_path
   elif [[ "$browser_type" == "firefox" ]]
   then
      sed -i s#temp_browser_type#Firefox#g $test_config_path
      sed -i s#temp_browser_options_type#FirefoxOption#g $test_config_path
   elif [[ "$browser_type" == "edge" ]]
   then
      sed -i s#temp_browser_type#Edge#g $test_config_path
      sed -i s#temp_browser_options_type#EdgeOption#g $test_config_path
   else
      echo "insertJenkinsParamsToTestScripts, ${browser_type} param is error:$browser_type"
   fi
}
#生成部署到k8s集群的yaml文件
generateDeployYamlFile(){
   echo "generateDeployYamlFile-------Start generate deployfile"
   getDeployTemplateFile
   JobName=${jobName}"-"${currentTime}
   PodLableName=$browser_type$"-"${codeversion}"-"${currentTime}
   PodNumbers=$browser_numbers
   k8s_node=$default_node
   ImageName=$imageName

   sed -i s#temp_PodLableName#${PodLableName}#g $deployFile
   sed -i s#temp_JobName#${JobName}#g $deployFile
   sed -i s#temp_PodNumbers#${PodNumbers}#g $deployFile
   sed -i s#temp_k8s-node#${k8s_node}#g $deployFile
   sed -i s#temp_ImageName#${ImageName}#g $deployFile
   echo "generateDeployYamlFile-------Finish to generate deployfile"
}
#generateDeployYamlFile
#这个方法应该可以添加到容器启动脚本里面,但是不想再重新做镜像,就添加到这个位置
setDockerConfigForHarbor(){
   echo "Get harbor website account and password"
   mkdir -p $dockerConfigPath
   cat $dockerConfigMapPath > $dockerConfigPath/config.json
}

#根据参数,选择部署最新或者回滚应用
deploy_action(){
   setDockerConfigForHarbor
   if [[ "$deploy_action" == "deploy" ]]
   then
      deploy
   elif [[ "$deploy_action" == "rollback" ]]
   then
      rollback
   elif [[ "$deploy_action" == "stop" ]]
   then
      stop
   else
      echo "deploy_action function, param is error:$deploy_action"
   fi
}
deploy_action
root@k8s-master1:~/k8s/jenkinsnode# 

c.将模板文件和脚本文件,创建成k8s中的configMap

此configMap,我们还是在上一篇博客的基础上整,所以第一步,我们在k8s的master节点,先删除configMap(当然,也可以通过打补丁的方式,我觉得删除后,再创建比较方便)

kubectl delete cm -n devops deploy-app

第二步,我们创建configmap,前面三个文件是上一篇博客,后面的2个文件,是我们本次添加进去的。

kubectl create configmap deploy-app -n devops --from-file=deploy_app.sh=deploy_app.sh  --from-file=deploy_springboot_app_template.yaml=deploy_springboot_app_template.yaml --from-file=config.json=config.json --from-file=deploy_uitestdemo.sh=deploy_uitestdemo.sh --from-file=job_uitestdemo_browser_template.yaml

5.在jenkins创建项目

a.安装allure插件

关于allure安装插件,比较简单,可以参考这个篇博客,jenkins集成allure
关于allure的插件配置,可以参考这篇博客,如何利用jenkins插件查看allure报告-----完整篇

b.创建jenkins项目

创建一个自由风格的项目,项目名称为uitestdemo

1.关于参数

总体效果是这样的
在这里插入图片描述jenkins的填写,是这样的
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

2.关于jenkins的node节点选择

因为目前仅仅是配置了jenkins的node1节点,所以,限制在jenkins1节点上运行
在这里插入图片描述

3.关联gitlab代码仓库

在这里插入图片描述

4.jenkins的构建脚本

脚本如下:

#1.定义生成的测试数据的路径
allureResultPath="/uitestdemotestdata/"$JOB_BASE_NAME"/"$BUILD_ID"/result"
allureReportPath="/uitestdemotestdata/"$JOB_BASE_NAME"/"$BUILD_ID"/report"
#2.定义新建部署根目录
deployWorkspace="/root/uitestdemo"
rm -rf $deployWorkspace
mkdir $deployWorkspace
#3.获取部署应用的脚本,jenkins的node节点,挂载了deploy-app这个configmap,我们的部署脚本就是从这里获取的
cd $deployWorkspace
cat /deploy/deploy_uitestdemo.sh >> $deployWorkspace/deploy_uitestdemo.sh
chmod +x deploy_uitestdemo.sh
#4.执行应用部署或回滚或停止
./deploy_uitestdemo.sh ${testCodeVersion} ${deploy_action} ${target_url} ${browser_type} ${browser_num} ${case_level} $BUILD_ID $JOB_BASE_NAME $WORKSPACE 
#5.由于我们配置了allure,我们只要将整合后的pod容器的测试结果目录,软连接到工作目录,因为我们配置了allure,allure会根据这些测试数据生成allure的测试报告
cd $WORKSPACE
rm -rf report result
ln -s $allureResultPath $WORKSPACE

截图如下:
在这里插入图片描述

5.allure的配置

在这里插入图片描述

6.遇到的问题

在部署测试镜像的时候,会发现启动测试脚本的问题,还是因为默认的共享内存问题,关于解决此类问题,可以参考博客,
UnknownError: session deleted because of page crash from tab crashed
Kubernetes(k8s) docker 修改 /dev/shm大小

七.验证

1.第一步,使用git提交代码到gitlab代码仓库
在这里插入图片描述2.第2步,开始build,对百度网站进行测试
在这里插入图片描述
3.在k8s集群,查看相关的job和pod信息
查看job信息
在这里插入图片描述
查看pod信息

在这里插入图片描述4.在harbor网站查看镜像情况
在这里插入图片描述5.在jenkins查看allure测试报告

在这里插入图片描述
在这里插入图片描述

八.遗留问题

1.关于测试报告的整合,目前是通过脚本动态的修改类名,来整合不同pod容器的测试结果数据,暂时未通过修改源码的方式,后面有空,还是通过修改pytest源码的方式比较好,通用技术 allure 报告合并的粗暴解决方案 (附带 pytest 部分源码分析)

九.参考资料

linux 限制ssh登录用户登录,如何限制SSH用户访问Linux中指定的目录
https://blog.csdn.net/weixin_35370061/article/details/116553412

11月22日早上上班,开机,发现我的开发机器的WiFi无法使用,尝试了各种办法都不行,我回想了一下,昨天晚上的操作,
点击更新系统后,就走了,然后,把内核版本降级,发现WiFi就可以用了,真是防不胜防,太坑。
Ubuntu 22用指定版本内核启动

关于selenium-python的文档,我感觉相当不错,mark
https://python-selenium-zh.readthedocs.io/zh_CN/latest/

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐