Python教程

python+pytest+selenium+allure实战

本文主要是介绍python+pytest+selenium+allure实战,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

selenium是一个针对web端项目的模拟鼠标和键盘操作的自动化测试工具,pytest是一个和unittest类似的自动化测试框架,但它比unittest更加方便,并且可以兼容unittest框架。

项目结构

在这里插入图片描述

  • common:存放公共方法,比如读取配置文件
  • config:存放配置文件。
  • logs:存放日志。
  • page:对selenium方法进行二次封装。
  • page_element:存放页面元素。
  • page_object:页面对象设计,将每个页面的功能均封装在这里,然后再testcase中调用,便于维护。
  • script:一些额外的脚本文件,我这里放的是检测页面元素格式的文件。
  • TestCase:存放测试用例。
  • utils:工具类。
  • conftest:pytest的配置文件。
  • run_case.py:配置生成allure报告的批处理文件,不影响整个测试用例的执行。

一、在config中创建config.ini和conf.py

config.ini写入host信息

[HOST]
HOST=http://rework.dfrobot.work/login

conf.py中配置文件目录、定位类型及邮箱信息

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import os,sys
from selenium.webdriver.common.by import By
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))))
from utils.times import dt_strftime


class ConfigManager(object):
    # 项目目录
    BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

    # 页面元素目录
    ELEMENT_PATH = os.path.join(BASE_DIR, 'page_element')

    # 报告文件
    REPORT_FILE = os.path.join(BASE_DIR, 'report.html')

    # 元素定位的类型
    LOCATE_MODE = {
        'css': By.CSS_SELECTOR,
        'xpath': By.XPATH,
        'name': By.NAME,
        'id': By.ID,
        'class': By.CLASS_NAME
    }

    # 邮件信息
    EMAIL_INFO = {
        'username': 'xxxxxxxx@qq.com',  # 切换成你自己的地址
        'password': '开启smtp服务后的授权码',  #开启smtp服务后的授权码,在qq邮箱-设置-账户中可以开启smtp服务并获取授权码
        'smtp_host': 'smtp.qq.com',
        'smtp_port': 465
    }

    # 收件人
    ADDRESSEE = [
        'xxxxxxxx@qq.com',
    ]

    @property  #创建只读属性
    def log_file(self):
        """日志目录"""
        log_dir = os.path.join(self.BASE_DIR, 'logs')
        if not os.path.exists(log_dir):
            os.makedirs(log_dir)
        return os.path.join(log_dir, '{}.log'.format(dt_strftime()))

    @property
    def ini_file(self):
        """配置文件"""
        ini_file = os.path.join(self.BASE_DIR, 'config', 'config.ini')
        if not os.path.exists(ini_file):
            raise FileNotFoundError("配置文件%s不存在!" % ini_file)
        return ini_file


cm = ConfigManager()
# if __name__ == '__main__':
#     print(cm.BASE_DIR)

二、common中创建readconfig.py和readelement.py

#readconfig.py
import configparser
import sys,os
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))))

from config.conf import cm 

HOST='HOST'

class ReadConfig(object):
    '''配置文件'''
    def __init__(self):
        self.config=configparser.RawConfigParser()
        self.config.read(cm.ini_file,encoding='utf-8')

    def _get(self,section,option):
        '''获取'''
        return self.config.get(section,option)

    def _set(self, section, option, value):
        """更新"""
        self.config.set(section, option, value)
        with open(cm.ini_file, 'w') as f:
            self.config.write(f)
    @property
    def url(self):
        return self._get(HOST, HOST)


ini = ReadConfig()

# if __name__ == '__main__':
#     print(ini.url)
#readelement.py
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import os,sys
import yaml
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))))
from config.conf import cm


class Element(object):
    """获取元素"""

    def __init__(self, name):
        self.file_name = '%s.yaml' % name
        self.element_path = os.path.join(cm.ELEMENT_PATH, self.file_name)
        if not os.path.exists(self.element_path):
            raise FileNotFoundError("%s 文件不存在!" % self.element_path)
        with open(self.element_path, encoding='utf-8') as f:
            self.data = yaml.safe_load(f)

    def __getitem__(self, item):
        """获取属性"""
        data = self.data.get(item)
        if data:
            name, value = data.split('==')
            return name, value
        raise ArithmeticError("{}中不存在关键字:{}".format(self.file_name, item))


# if __name__ == '__main__':
#     overview = Element('overview')
#     print(overview['首页按钮'])

三、在utils中添加工具类

#logger.py
import ctypes,sys

STD_INPUT_HANDLE = -10
STD_OUTPUT_HANDLE = -11
STD_ERROR_HANDLE = -12

FOREGROUND_BLACK = 0x00 # black.
FOREGROUND_DARKBLUE = 0x01 # dark blue.
FOREGROUND_DARKGREEN = 0x02 # dark green.
FOREGROUND_DARKSKYBLUE = 0x03 # dark skyblue.
FOREGROUND_DARKRED = 0x04 # dark red.
FOREGROUND_DARKPINK = 0x05 # dark pink.
FOREGROUND_DARKYELLOW = 0x06 # dark yellow.
FOREGROUND_DARKWHITE = 0x07 # dark white.
FOREGROUND_DARKGRAY = 0x08 # dark gray.
FOREGROUND_BLUE = 0x09 # blue.
FOREGROUND_GREEN = 0x0a # green.
FOREGROUND_SKYBLUE = 0x0b # skyblue.
FOREGROUND_RED = 0x0c # red.
FOREGROUND_PINK = 0x0d # pink.
FOREGROUND_YELLOW = 0x0e # yellow.
FOREGROUND_WHITE = 0x0f # white.

BACKGROUND_BLUE = 0x10 # dark blue.
BACKGROUND_GREEN = 0x20 # dark green.
BACKGROUND_DARKSKYBLUE = 0x30 # dark skyblue.
BACKGROUND_DARKRED = 0x40 # dark red.
BACKGROUND_DARKPINK = 0x50 # dark pink.
BACKGROUND_DARKYELLOW = 0x60 # dark yellow.
BACKGROUND_DARKWHITE = 0x70 # dark white.
BACKGROUND_DARKGRAY = 0x80 # dark gray.
BACKGROUND_BLUE = 0x90 # blue.
BACKGROUND_GREEN = 0xa0 # green.
BACKGROUND_SKYBLUE = 0xb0 # skyblue.
BACKGROUND_RED = 0xc0 # red.
BACKGROUND_PINK = 0xd0 # pink.
BACKGROUND_YELLOW = 0xe0 # yellow.
BACKGROUND_WHITE = 0xf0 # white.

std_out_handle = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
def set_cmd_text_color(color, handle=std_out_handle):
    Bool = ctypes.windll.kernel32.SetConsoleTextAttribute(handle, color)
    return Bool

def reset():
    set_cmd_text_color(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)

def error(mess, end = '\n', flush = True):
    set_cmd_text_color(FOREGROUND_RED)
    print("[ERROR]", mess, end = end, flush = flush)
    reset()

def warn(mess, end = '\n', flush = True):
    set_cmd_text_color(FOREGROUND_YELLOW)
    print("[WARN]", mess, end = end, flush = flush)
    reset()

def info(mess, end = '\n', flush = True):
    set_cmd_text_color(FOREGROUND_GREEN)
    print("[INFO]", mess, end = end, flush = flush)
    reset()

def write(mess, foregound = FOREGROUND_WHITE, background = FOREGROUND_BLACK, end = '\n', flush = True):
    set_cmd_text_color(foregound | background)
    print(mess, end = end, flush = flush)
    reset()

# if __name__=='__main__':
#     info("======")
#times.py
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import time
import datetime
from functools import wraps


def timestamp():
    """时间戳"""
    return time.time()


def dt_strftime(fmt="%Y%m"):
    """
    datetime格式化时间
    :param fmt "%Y%m%d %H%M%S
    """
    return datetime.datetime.now().strftime(fmt)


def sleep(seconds=1.0):
    """
    睡眠时间
    """
    time.sleep(seconds)


def running_time(func):
    """函数运行时间"""

    @wraps(func) # @wraps 不改变使用装饰器原有函数的结构
    def wrapper(*args, **kwargs):
        start = timestamp()
        res = func(*args, **kwargs)
        print("校验元素done!用时%.3f秒!" % (timestamp() - start))
        return res

    return wrapper


# if __name__ == '__main__':
#     print(dt_strftime("%Y-%m-%d %H:%M:%S"))

四、page中创建webpage.py

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
"""
selenium基类
本文件存放了selenium基类的封装方法
"""
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
import sys,os
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))))

from config.conf import cm
from utils.times import sleep
from utils import logger


class WebPage(object):
    """selenium基类"""

    def __init__(self, driver):
        # self.driver = webdriver.Chrome()
        self.driver = driver
        self.timeout = 20
        self.wait = WebDriverWait(self.driver, self.timeout)

    def get_url(self, url):
        """打开网址并验证"""
        
        self.driver.set_page_load_timeout(60)
        try:
            self.driver.get(url)
            self.driver.implicitly_wait(10)
            logger.info("打开网页:%s" % url)
        except TimeoutException:
            raise TimeoutException("打开%s超时请检查网络或网址服务器" % url)

    @staticmethod
    def element_locator(func, locator):
        """元素定位器"""
        name, value = locator
        return func(cm.LOCATE_MODE[name], value)

    def find_element(self, locator):
        """寻找单个元素"""
        return WebPage.element_locator(lambda *args: self.wait.until(
            EC.presence_of_element_located(args)), locator)  #presence_of_element_located((By.ID,"acdid")) 显式等待

    def get_attrib(self, locator,value):
        """获取元素属性"""
        logger.info("获取属性")
        ele=self.find_element(locator)
        sleep(0.5)
        return ele.get_attribute(value)
        # js='document.querySelector("#质检表_返工单号").value'
        # self.driver.execute_script(js)
        
        
    def find_elements(self, locator):
        """查找多个相同的元素"""
        return WebPage.element_locator(lambda *args: self.wait.until(
            EC.presence_of_all_elements_located(args)), locator)

    def find_element_drag(self,locator):
        target = self.find_element(locator)
        self.driver.execute_script("arguments[0].scrollIntoView();", target) #拖动到可见的元素去

    def elements_num(self, locator):
        """获取相同元素的个数"""
        number = len(self.find_elements(locator))
        logger.info("相同元素:{}".format((locator, number)))
        return number

    def input_text(self, locator, txt):
        """输入(输入前先清空)"""
        sleep(0.5)
        ele = self.find_element(locator)
        ele.clear()
        ele.send_keys(txt)
        sleep(0.5)
        logger.info("输入文本:{}".format(txt))

    def input_enter(self, locator):
        """回车、tab等键入"""
        
        ele = self.find_element(locator)
        ele.send_keys(Keys.ENTER)


    def is_click(self, locator):
        """点击"""
        self.find_element(locator).click()
        sleep()
        logger.info("点击元素:{}".format(locator))

    def element_text(self, locator):
        """获取当前的text"""
        _text = self.find_element(locator).text
        logger.info("获取文本:{}".format(_text))
        return _text

    def hold_on(self,locator):
        #定位到要悬停的元素
        move = self.find_element(locator)
        #对定位到的元素执行悬停操作
        ActionChains(self.driver).move_to_element(move).perform()
        sleep()
        logger.info("悬停元素:{}".format(locator))

    def screen_scoll(self):
        self.driver.execute_script('window.scrollBy(0, 300)')
        sleep(1)
    @property
    def get_source(self):
        """获取页面源代码"""
        return self.driver.page_source

    def refresh(self):
        """刷新页面F5"""
        self.driver.refresh()
        self.driver.implicitly_wait(30)

五、page_element下创建login.yaml文件,记录元素位置

账号: 'xpath==//*[@id="login_username"]'
密码: 'xpath==//*[@id="login_password"]'  
登录: 'xpath==//*[@id="login"]/div[4]/div/div/div/button'

六、在script下创建inspect.py

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import os,sys
import yaml
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))))
from config.conf import cm
from utils.times import running_time


@running_time
def inspect_element():
    """检查所有的元素是否正确
    只能做一个简单的检查
    """
    for files in os.listdir(cm.ELEMENT_PATH):
        _path = os.path.join(cm.ELEMENT_PATH, files)
        with open(_path, encoding='utf-8') as f:
            data = yaml.safe_load(f)
        for k in data.values():
            try:
                pattern, value = k.split('==')
            except ValueError:
                raise Exception("{} : {} 元素表达式中没有`==`".format(_path,k))
            if pattern not in cm.LOCATE_MODE:
                raise Exception('%s中元素【%s】没有指定类型' % (_path, k)
            else:
                assert value, '%s中元素【%s】类型与值不匹配' % (_path, k)


if __name__ == '__main__':
    inspect_element()

七、在page_object下创建login.py

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import sys,os
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))))

from page.webpage import WebPage
from common.readelement import Element

login = Element('login')  #获取login.yaml

class LoginPage(WebPage):
    '''登录'''
    def input_user(self,content):
        self.input_text(login['账号'],content)

    def input_pwd(self,content):
        self.input_text(login['密码'],content)
    
    def btn_login(self):
        self.is_click(login['登录'])

八、在Test_case中创建测试用例test_001_main.py

# -*- coding:utf-8 -*-
import re,os,sys
import allure
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))))

from utils.times import sleep
import pytest
from pytest import assume
from utils import logger
from utils.times import sleep,dt_strftime
from common.readconfig import ini
from page_object.login import LoginPage

@allure.story("测试返工主流程:顺利通过的全套流程")
class TestOverview:

    @allure.step("登录")
    @pytest.fixture(scope="function")
    def login(self, drivers):
        """登录"""
        login = LoginPage(drivers)
        login.get_url(ini.url)
        login.input_user('xxxxx')
        login.input_pwd('xxxxxx')
        login.btn_login()
        
	@allure.step("登录后的操作")
    @pytest.mark.usefixtures("login")
    def test_001(self, drivers):
        """登录后操作"""    
        print("登录后操作")
        
#pytest会自动搜索测试用例,不用在这里调用,这里只是为了单个文件调试的时候使用
# if __name__  == '__main__' : 
#     pytest.main(['test_001_main.py','-s'])#'--capture=no'

九、在根目录下添加conftest.py

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import pytest
from py.xml import html
from selenium import webdriver


driver = None


@pytest.fixture(scope='session', autouse=True)
def drivers(request):
    global driver
    if driver is None:
        driver = webdriver.Chrome()
        driver.maximize_window()
        

    def fn():
        driver.quit()

    request.addfinalizer(fn)
    return driver


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item):
    """
    当测试失败的时候,自动截图,展示到html报告中
    :param item:
    """
    pytest_html = item.config.pluginmanager.getplugin('html')
    outcome = yield
    report = outcome.get_result()
    report.description = str(item.function.__doc__)
    extra = getattr(report, 'extra', [])



def pytest_html_results_table_header(cells):
    cells.insert(1, html.th('用例名称'))
    cells.insert(2, html.th('Test_nodeid'))
    cells.pop(2)



def pytest_html_results_table_html(report, data):
    if report.passed:
        del data[:]
        data.append(html.div('通过的用例未捕获日志输出.', class_='empty log'))


def _capture_screenshot():
    '''
    截图保存为base64
    :return:
    '''
    return driver.get_screenshot_as_base64()

十、在根目录下新建pytest.ini文件,对pytest执行过程中的操作做全局控制

[pytest]
addopts = --html=report.html --self-contained-html

十一、执行

在根目录下,在cmd中直接输入pytest,会自动搜索测试用例,执行完成后在根目录下输出html报告。
在这里插入图片描述

十二、在utils下创建send_mail.py发送邮件

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import zmail
import sys,os
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))))

from config.conf import cm


def send_report():
    """发送报告"""
    with open(cm.REPORT_FILE, encoding='utf-8') as f:
        content_html = f.read()
    try:
        mail = {
            'from': 'xxxxxxxxxx@qq.com',
            'subject': '测试报告',
            'content_html': content_html,
            'attachments': [cm.REPORT_FILE, ]
        }
        server = zmail.server(*cm.EMAIL_INFO.values())
        server.send_mail(cm.ADDRESSEE, mail)
        print("测试邮件发送成功!")
    except Exception as e:
        print("Error: 无法发送邮件,{}!", format(e))


if __name__ == "__main__":
    '''请先在config/conf.py文件设置QQ邮箱的账号和密码'''
    send_report()

十二、生成allure报告

需要先安装allure,这里在我的另一个文章python3+unittest+selenium自动化实战
中有详细介绍,但是在那篇文章里,使用了命令行的方式来打开allure server,需要输入多次命令。这里为了简化操作,将所有命令写入一个py文件中,我们只需要运行这个py文件,就可以执行测试用例,并且自动打开生成的allure报告。
因此,在根目录下创建一个run_case.py文件

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import sys
import subprocess

WIN = sys.platform.startswith('win')


def main():
   """主函数"""
   steps = [
       "venv\\Script\\activate" if WIN else "source venv/bin/activate",
       "pytest --alluredir allure-results --clean-alluredir",
       "allure generate allure-results -c -o allure-report",
       "allure open allure-report"
   ]
   for step in steps:
       subprocess.run("call " + step if WIN else step, shell=True)


if __name__ == "__main__":
   main()

在这里插入图片描述

这篇关于python+pytest+selenium+allure实战的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!