0


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

classConfigManager(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':'[email protected]',# 切换成你自己的地址'password':'开启smtp服务后的授权码',#开启smtp服务后的授权码,在qq邮箱-设置-账户中可以开启smtp服务并获取授权码'smtp_host':'smtp.qq.com','smtp_port':465}# 收件人
    ADDRESSEE =['[email protected]',]@property#创建只读属性deflog_file(self):"""日志目录"""
        log_dir = os.path.join(self.BASE_DIR,'logs')ifnot os.path.exists(log_dir):
            os.makedirs(log_dir)return os.path.join(log_dir,'{}.log'.format(dt_strftime()))@propertydefini_file(self):"""配置文件"""
        ini_file = os.path.join(self.BASE_DIR,'config','config.ini')ifnot 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.pyimport configparser
import sys,os
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__),'../'))))from config.conf import cm 

HOST='HOST'classReadConfig(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)withopen(cm.ini_file,'w')as f:
            self.config.write(f)@propertydefurl(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

classElement(object):"""获取元素"""def__init__(self, name):
        self.file_name ='%s.yaml'% name
        self.element_path = os.path.join(cm.ELEMENT_PATH, self.file_name)ifnot os.path.exists(self.element_path):raise FileNotFoundError("%s 文件不存在!"% self.element_path)withopen(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.pyimport 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)defset_cmd_text_color(color, handle=std_out_handle):
    Bool = ctypes.windll.kernel32.SetConsoleTextAttribute(handle, color)return Bool

defreset():
    set_cmd_text_color(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)deferror(mess, end ='\n', flush =True):
    set_cmd_text_color(FOREGROUND_RED)print("[ERROR]", mess, end = end, flush = flush)
    reset()defwarn(mess, end ='\n', flush =True):
    set_cmd_text_color(FOREGROUND_YELLOW)print("[WARN]", mess, end = end, flush = flush)
    reset()definfo(mess, end ='\n', flush =True):
    set_cmd_text_color(FOREGROUND_GREEN)print("[INFO]", mess, end = end, flush = flush)
    reset()defwrite(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

deftimestamp():"""时间戳"""return time.time()defdt_strftime(fmt="%Y%m"):"""
    datetime格式化时间
    :param fmt "%Y%m%d %H%M%S
    """return datetime.datetime.now().strftime(fmt)defsleep(seconds=1.0):"""
    睡眠时间
    """
    time.sleep(seconds)defrunning_time(func):"""函数运行时间"""@wraps(func)# @wraps 不改变使用装饰器原有函数的结构defwrapper(*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

classWebPage(object):"""selenium基类"""def__init__(self, driver):# self.driver = webdriver.Chrome()
        self.driver = driver
        self.timeout =20
        self.wait = WebDriverWait(self.driver, self.timeout)defget_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)@staticmethoddefelement_locator(func, locator):"""元素定位器"""
        name, value = locator
        return func(cm.LOCATE_MODE[name], value)deffind_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")) 显式等待defget_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)deffind_elements(self, locator):"""查找多个相同的元素"""return WebPage.element_locator(lambda*args: self.wait.until(
            EC.presence_of_all_elements_located(args)), locator)deffind_element_drag(self,locator):
        target = self.find_element(locator)
        self.driver.execute_script("arguments[0].scrollIntoView();", target)#拖动到可见的元素去defelements_num(self, locator):"""获取相同元素的个数"""
        number =len(self.find_elements(locator))
        logger.info("相同元素:{}".format((locator, number)))return number

    definput_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))definput_enter(self, locator):"""回车、tab等键入"""
        
        ele = self.find_element(locator)
        ele.send_keys(Keys.ENTER)defis_click(self, locator):"""点击"""
        self.find_element(locator).click()
        sleep()
        logger.info("点击元素:{}".format(locator))defelement_text(self, locator):"""获取当前的text"""
        _text = self.find_element(locator).text
        logger.info("获取文本:{}".format(_text))return _text

    defhold_on(self,locator):#定位到要悬停的元素
        move = self.find_element(locator)#对定位到的元素执行悬停操作
        ActionChains(self.driver).move_to_element(move).perform()
        sleep()
        logger.info("悬停元素:{}".format(locator))defscreen_scoll(self):
        self.driver.execute_script('window.scrollBy(0, 300)')
        sleep(1)@propertydefget_source(self):"""获取页面源代码"""return self.driver.page_source

    defrefresh(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_timedefinspect_element():"""检查所有的元素是否正确
    只能做一个简单的检查
    """for files in os.listdir(cm.ELEMENT_PATH):
        _path = os.path.join(cm.ELEMENT_PATH, files)withopen(_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 notin 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.yamlclassLoginPage(WebPage):'''登录'''definput_user(self,content):
        self.input_text(login['账号'],content)definput_pwd(self,content):
        self.input_text(login['密码'],content)defbtn_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("测试主流程:顺利通过的全套流程")classTestOverview:@allure.step("登录")@pytest.fixture(scope="function")deflogin(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")deftest_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 [email protected](scope='session', autouse=True)defdrivers(request):global driver
    if driver isNone:
        driver = webdriver.Chrome()
        driver.maximize_window()deffn():
        driver.quit()

    request.addfinalizer(fn)return driver

@pytest.hookimpl(hookwrapper=True)defpytest_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',[])defpytest_html_results_table_header(cells):
    cells.insert(1, html.th('用例名称'))
    cells.insert(2, html.th('Test_nodeid'))
    cells.pop(2)defpytest_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()

这里要注意创建的drivers函数,因为添加了@pytest.fixture(scope=‘session’, autouse=True)修饰器,这个函数会在session级别的testcase中生效 ,并返回webdriver。在Testoverview类里,每个testcase,如test_001中都传递了一个“drivers”参数,这个drivers就是调用的conftest中的drivers函数。

十、在根目录下新建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

defsend_report():"""发送报告"""withopen(cm.REPORT_FILE, encoding='utf-8')as f:
        content_html = f.read()try:
        mail ={'from':'[email protected]','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')defmain():"""主函数"""
   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()

在这里插入图片描述


本文转载自: https://blog.csdn.net/tianshuiyimo/article/details/120099423
版权归原作者 tianshuiyimo 所有, 如有侵权,请联系我们删除。

“python+pytest+selenium+allure实战”的评论:

还没有评论