Python+Selenium+Pytest+Allure+Jenkins实现的Web自动化框架

发布时间 2023-12-29 18:08:55作者: hqq的进阶日记

一、测试的项目

jpress-4.1.5 博客搭建平台

Linux服务器环境:JDK(1.8.0)+Tomcat(apache-tomcat-8.5.91)+MySql(8.0)

准备工作:将jpress-4.1.5.war 上传到服务器,复制到 /usr/local/tomcat/webapps 目录下,会自动解压缩。

访问:http://服务器ip:端口/jpress-4.1.5/admin

二、需求分析

本项目要测试的是一个开源的Java项目,测试的功能包括:

  1. 用户注册
  2. 用户登录
  3. 后台管理员登录
  4. 添加文章
  5. 删除文章

本项目通过自动化测试的方法来验证,用户注册、用户登录、管理员登录、添加文章、信息的完整性、正确性。比如必填项、电子邮件的格式、密码一致性、识别验证码。

其中,识别验证码是本项目的难点,通过调用第三方接口实现。

三、用例设计-部分用例举例

四、框架说明

本框架是一套基于 Python+Selenium+Pytest+Allure+Jenkins 而设计的数据驱动Web自动化测试的框架。

  • 技术栈:Python、Pytest、Excel、Json、MySql、Allure、Jenkins。

4.1 测试框架结构图如下:

4.2 项目功能

Python+Selenium+Pytest+Allure+Jenkins web自动化框架,使用Page Object设计模式,将页面的元素和元素之间的操作方法进行分离。它有三层架构,分别为:基础封装层BasePage,PO页面对象层,TestCase测试用例层。

同时使用DDT数据驱动测试思想,将测试数据和测试用例分离,提高代码复用率,减少重复代码的编写。

该框架能够结合 Pytest 进行单元测试,记录日志,并生成 allure 测试报告,最后进行 Jenkins 集成项目实现集成部署,并发送测试报告邮件。

五、代码设计与功能说明

5.1 POM简介:Page Object Modle页面对象模型

  1. 页面对象模型(POM)是一种设计模式,用来管理维护一组web元素集的对象库
  2. 在POM下,应用程序的每一个页面都有一个对应的Page Class
  3. 每一个Page Class 维护着该web页的元素集合、操作这些元素的方法
  4. Page Class中的方法命名最好根据对应的业务场景进行

中心思想:定位元素和测试用例分开,实现代码松耦合的效果

5.2 基础封装层:pages/basePage.py

点击查看代码
class BasePage(object):
    def __init__(self,driver):
        self.driver = driver

    def find_element(self,*loc):
        return self.driver.find_element(*loc)

    def type_text(self,text,*loc):
        self.find_element(*loc).send_keys(text)

    def click(self,*loc):
        self.find_element(*loc).click()

    def clear(self,*loc):
        self.find_element(*loc).clear()

    def get_title(self):
        return self.driver.title

封装查找元素、输入测试数据、点击、清空等常用操作方法。

5.3 PO页面对象层:pages/userLoginPage.py

点击查看代码
from time import sleep
from selenium.webdriver.common.by import By
from pages.basePage import BasePage

class UserLoginPage(BasePage):
    username_input = (By.NAME,'user')
    pwd_input = (By.NAME,'pwd')
    captcha_input = (By.NAME, 'captcha')
    login_btn = (By.CLASS_NAME,'btn')

    def __init__(self,driver):
        BasePage.__init__(self,driver)

    def goto_login_page(self):
        self.driver.get('http://192.168.10.184:8072/jpress-4.1.5/user/login')

    def input_username(self,username):
        self.clear(*self.username_input)
        self.type_text(username,*self.username_input)

    def input_pwd(self,pwd):
        self.clear(*self.pwd_input)
        self.type_text(pwd, *self.pwd_input)

    def input_captcha(self,captcha):
        self.clear(*self.captcha_input)
        self.type_text(captcha,*self.captcha_input)

    def click_login_btn(self):
        self.click(*self.login_btn)

pages/userLoginPage.py 维护了用户登录页面的元素集合:用户名、密码、验证码、登录按钮;操作这些元素的方法:进入用户登录页面、输入用户名、输入密码、输入验证码、点击登录按钮。

5.4 TestCase测试用例层:testcases/testUserLogin.py

点击查看代码
# -*- coding: utf-8 -*-
import pytest
from selenium.webdriver.support.wait import WebDriverWait
from pages.userLoginPage import UserLoginPage
from selenium.webdriver.support import expected_conditions as EC
from selenium import webdriver
from time import sleep
from util import util
from selenium.webdriver.common.by import By

class TestUserLogin(object):
    login_data=[
        ('','admin','这是必填内容'),
        ('admin', '', '这是必填内容'),
        ('admin', 'admin', '用户中心')
    ]

    def setup_class(self):
        option = webdriver.ChromeOptions()
        # 无头模式
        option.add_argument('headless')
        # 沙盒模式运行
        option.add_argument('no-sandbox')
        # 渲染时写入/tmp
        option.add_argument('disable-dev-shm-usage')
        # 专门应对无头浏览器不能最大化屏幕的方案
        option.add_argument("--window-size=1920,1050")

        self.driver = webdriver.Chrome(options=option)
        self.loginPage = UserLoginPage(self.driver)
        self.loginPage.goto_login_page()
        self.logger = util.get_logger()


    # 测试用户登录
    @pytest.mark.parametrize('username,pwd,expected',login_data)
    def test_user_login_username(self,username,pwd,expected):
        # 输入用户名
        self.loginPage.input_username(username)
        # 输入密码
        self.loginPage.input_pwd(pwd)
        # 记录日志
        self.logger.debug('输入用户名称:%s,输入的用户密码:%s',username,pwd)

        if username=='':
            # 点击登录
            self.loginPage.click_login_btn()
            sleep(1)
            try:
                # assert self.driver.find_element_by_id('user-error').text == expected
                assert self.driver.find_element(By.ID,'user-error').text == expected
            except AssertionError as ae:
                self.logger.error("报错,报错:%s", "用户名为空登录", exc_info=1)
        elif pwd == '':
            # 点击登录
            self.loginPage.click_login_btn()
            sleep(1)
            try:
                # assert self.driver.find_element_by_id('pwd-error').text == expected
                assert self.driver.find_element(By.ID,'pwd-error').text == expected
            except AssertionError as ae:
                self.logger.error("报错,报错:%s", "密码为空登录", exc_info=1)
        else:
            # 自动识别验证码
            captcha = util.get_code(self.driver, 'captcha-img')
            # 输入验证码
            self.loginPage.input_captcha(captcha)
            # 点击登录
            self.loginPage.click_login_btn()
            # 等待提示框
            WebDriverWait(self.driver, 5).until(EC.title_is(expected))
            sleep(1)
            # 验证
            try:
                assert self.driver.title == expected
            except AssertionError as ae:
                self.logger.error("报错,报错:%s", "登录", exc_info=1)

if __name__ == '__main__':
    pytest.main(['testUserLogin.py'])

说明:

1)使用chrome的headless无头模式,是因为在Linux服务器上执行该脚本时,服务器上的Chrome无GUI。

2)使用了ddt的思想,将测试数据和测试脚本分离,且一套测试脚本可以执行多组不同测试数据的用例。

3)调用了日志模块,将脚本运行过程日志及报错日志,记录到日志文件中,方便排查问题。

5.5 工具类模块:解决验证码识别、日志打印等问题

点击查看代码

import base64
import json
import os
import pickle
import random
import string
import time
import requests
from PIL import Image
from selenium import webdriver
from selenium.webdriver.common.by import By
import logging.handlers
import datetime

_custom_url = "http://api.jfbym.com/api/YmServer/customApi"
_token = "Z-lvHdVz4hyuYT8_SmrG53VDH7EW4req4mpkN1rQIeY"
_headers = {
    'Content-Type': 'application/json'
}


def common_verify(image, verify_type="10110"):
    # 10110 通用数英1-4位
    payload = {
        "image": base64.b64encode(image).decode(),
        "token": _token,
        "type": verify_type
    }
    resp = requests.post(_custom_url, headers=_headers, data=json.dumps(payload))
    print(resp.text)
    return resp.json()['data']['data']

# id 是要截图的元素的id
def save_img(driver,id):
    # 获取验证码图片
    t = time.time()
    path = os.path.dirname(os.path.dirname(__file__)) + '\\screenshots'
    picture_name1 = path + '\\' + str(t) + '.png'

    driver.save_screenshot(picture_name1)
    # ce = driver.find_element_by_id(id)  # id = captchaimg
    ce = driver.find_element(By.ID,id)

    left = ce.location['x']
    top = ce.location['y']
    right = ce.size['width'] + left
    height = ce.size['height'] + top

    im = Image.open(picture_name1)
    # 抠图
    img = im.crop((left, top, right, height))
    t = time.time()
    picture_name2 = path + '\\' + str(t) + '.png'
    img.save(picture_name2)  # 把验证码的图保存下来
    return picture_name2

def get_code(driver,id):
    picture = save_img(driver,id)
    with open(picture, 'rb') as f:
        s = f.read()
    return common_verify(s)

def gen_random_str():
    rand_str = ''.join(random.sample(string.ascii_letters + string.digits, 8))
    return rand_str

def save_cookie(driver, path):
    with open(path, 'wb') as filehandler:
        cookies = driver.get_cookies()
        print(cookies)
        pickle.dump(cookies, filehandler)


def load_cookie(driver, path):
    with open(path, 'rb') as cookiesfile:
        cookies = pickle.load(cookiesfile)
        for cookie in cookies:
            driver.add_cookie(cookie)

def get_logger():
    path = os.path.dirname(os.path.dirname(__file__)) + '\\logs\\'
    logger = logging.getLogger('mylogger')
    logger.setLevel(logging.DEBUG)

    rf_handler = logging.handlers.TimedRotatingFileHandler(path+'all.log',when='midnight',interval=1,backupCount=7,atTime=datetime.time(0,0,0,0))
    rf_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s"'))
    f_handler = logging.FileHandler(path+'error.log')
    f_handler.setLevel(logging.ERROR)
    f_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(filename)s[:%(lineno)d] - %(message)s"))

    logger.addHandler(rf_handler)
    logger.addHandler(f_handler)
    return logger

if __name__ == '__main__':
    driver = webdriver.Chrome()
    driver.get('http://192.168.10.184:8072/jpress-4.1.5/admin/login')
    id = "captcha-img"
    print(get_code(driver, id))

5.6 主运行方法:runall.py

点击查看代码
# -*- coding: utf-8 -*-
import pytest
import os
from testcases import *

if __name__ == '__main__':
    #批量执行文件夹下的测试用例
    pytest.main(['-s', '-v',
                 'testcases/testAdminLogin.py',
                 'testcases/testArticle.py',
                 'testcases/testUserLogin.py',
                 'testcases/testUserRegister.py',
                 '-q',
                 '--clean-alluredir',
                 '--alluredir', 'reports'])
    os.system("allure generate reports -c -o reports/allure_result/")

总结:

1)批量执行多个测试用例

2)生成Allure测试报告,并将测试报告文件放到 reports/allure_result/路径下

六、GitLab管理代码

虚拟机的Linux发行版:

前提条件:

  • 安装Docker
  • 安装Docker-compose

6.1 安装gitlab

1)服务器中保证有docker、docker-compose

$ docker version
$ docker compose version

2)访问安全
云服务器:设置安全组【阿里云服务器设置】
虚拟机:关闭防火墙

$ systemctl stop firewalld

3)在/usr/local下创建/docker/ gitlab_docker目录

$ cd /usr/local
$ sudo mkdir docker
$ cd docker
$ sudo mkdir gitlab_docker
$ cd gitlab_docker/

4)在/usr/local/docker/gitlab_docker下创建docker-compose.yml文件

$ sudo vi docker-compose.yml

用于编写启动docker的配置信息的文件
配置文件内容:

external_url说明:

  • 虚拟机:虚拟机的ip;
  • 云服务器:服务器的公网ip

5)重启docker,启动gitlab,会自动拉取镜像

$ sudo systemctl docker restart
$ sudo docker compose up -d

查看启动日志:

$ sudo docker logs –f gitlab

6)进入gitlab容器内部查看root密码

$ docker exec –it gitlab bash
$ cat /etc/gitlab/initial_root_password

该文件记录了初始密码(24h后删除),登陆成功后修改密码,如:
Ki91HCFAUcp6Lg1iXCQsT+5M7aFzj6IFOjk16NIvj/I=

2、登录gitlab修改用户密码
3、创建project,设置仓库

4、上传管理代码

七、Jenkins持续集成

7.1 安装JDK

版本:jdk-8u231-linux-x64.tar.gz

$ sudo tar zxvf jdk-8u151-linux-x64.tar.gz -C /usr/local
$ cd /usr/local
$ sudo mv jdk1.8.0_151/ jdk/

7.2 安装Jenkins

$ cd /usr/local
$ mkdir docker
$ cd docker
$ mkdir jenkins_docker
$ cd jenkins_docker/

创建docker-compose.yml文件:

重启docker、启动jenkins

$ sudo systemctl restart docker
启动:
$ sudo docker compose up –d
查看日志是否启动出现问题:
$ sudo docker logs –f jenkins

登录Jenkins、安装默认插件等。

7.3 配置Jenkins

1. 将 /usr/local下的jdk、maven挪到docker/jenkins_docker/data
/usr/local/docker/jenkins_docker/data$ sudo mv /usr/local/jdk/ ./

2. 指定容器卷下的jdk和jenkins
容器内的路径:/var/jenkins_home/
【系统管理-全局工具配置-JDK】
jdk配置:/var/jenkins_home/jdk

3.服务器配置/etc/profile文件:
# jdk env
export JAVA_HOME=/usr/local/docker/jenkins_docker/data/jdk
export PATH=$PATH:$JAVA_HOME/bin

4. 生效配置文件
$ source /etc/profile

7.4 Jenkins环境其他配置

1)Jenkins 容器内部安装Python3,方便在构建脚本中运行py文件

2)Jenkins 容器内部安装 Chrome浏览器和对应版本的chromedriver驱动

7.5 创建SeleniumJpress任务并配置

1)源码管理:从gitlab拉取代码

2)构建步骤:运行runall.py 脚本

3)构建后操作:Allure 报告的存放地址

4)构建后邮件通知:

7.6 系统的配置补充

1)环境变量:Jenkins容器中的环境变量

2)邮件通知:注意密码是授权码

3)SSH服务器配置

4)JDK

5)Aiiure Commandline

八、构建结果及报告查看

控制台输出:

Allure报告: