从零开始搭建一个简单的ui自动化测试框架03(pytest+selenium+allure)

简介: 三、填充我们的框架设计主类我们首先来实现我们的测试用例的主类设计,这个类主要用以被其他的测试用例继承,来实现一些每个测试用例都会做的事情,具体一点就是:继承unittest,创建一个webdriver的实例,以及每次运行用例时打开和关闭浏览器。

三、填充我们的框架

设计主类

我们首先来实现我们的测试用例的主类设计,这个类主要用以被其他的测试用例继承,来实现一些每个测试用例都会做的事情,具体一点就是:
继承unittest,创建一个webdriver的实例,以及每次运行用例时打开和关闭浏览器。

可能之后还有更多这样的共性的事情会被放到测试主类,到时候我们就继续在测试主类里添加。

我们在之前预留的位置maincase里新建一个py文件,在里面写这个主类MainCase。
(稍微的介绍一下unittest框架,unittest会在正式的测试用例开始之前执行setup方法,在执行用例结束后会执行teardown方法,测试用例是指以test开头的方法,例如test_something这样的名字)

# -*- coding: utf-8 -*-
from selenium import webdriver
from operate.baseoperate import BaseOperate

import unittest

# 主测试类继承自测试框架unittest
class MainCase(unittest.TestCase):
    # 声明一个webdriver
    driver = webdriver
    # 声明一个基础操作类base_operate
    base_operate = BaseOperate

    def setUp(self):
        # 测试之前,启动浏览器,打开一个设定好的网址
        self.driver = webdriver.Chrome()
        self.driver.get("http://open.qq.com")
        # 最大化窗口
        self.driver.maximize_window()
        # 传入webdriver,实例化这个类
        self.base_operate = BaseOperate(self.driver)

    def tearDown(self):
        # 测试完毕的时候关闭浏览器
        self.driver.quit()


if __name__ == '__main__':
    unittest.main()

这样我们的主类就设计好了,之后我们写正式的testcase的时候,只需要继承这个类就可以了。

设计基础操作类和config文件

为了让正式的用例写的更加的爽,我们还需要封装一些基础操作方便调用,这个类放到我们预留的位置operate里,名字叫做BaseOperate。

为了让这个类可以随时调用,我们在前面提到的测试主类里,把webdriver的实例给他,具体的做法是,为BaseOperate这个类设计一个构造函数,在MainCase里传入其webdriver实例。

BaseOperate这个类的代码如下:

# -*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import NoSuchElementException

class BaseOperate():
    # 声明一个webdriver类
    operate_driver = webdriver.Chrome

    def __init__(self, driver):
        # 构造函数里将driver传入
        self.operate_driver = driver

MainCase里修改一下,现在是这样:

# -*- coding: utf-8 -*-
from selenium import webdriver
from operate.baseoperate import BaseOperate
import unittest

# 主测试类继承自测试框架unittest
class MainCase(unittest.TestCase):
    # 实例化一个webdriver
    driver = webdriver.Chrome()
    # 声明一个基础操作类base_operate
    base_operate = BaseOperate

    def setUp(self):
        # 测试之前,启动浏览器,打开一个设定好的网址
        self.driver.get("http://open.qq.com")
        # 传入webdriver,实例化这个类
        self.base_operate = BaseOperate(self.driver)

    def tearDown(self):
        # 测试完毕的时候关闭浏览器
        self.driver.close()


if __name__ == '__main__':
    unittest.main()

ok,接下来我们的用例只需要继承自主测试类,就可以直接用base_operate里封装的代码了(后续会上这一块的代码),现在我们要做的是把这个基础操作类填充一下。

想一下,我们要封装什么基础操作才会更加方便我们用例的书写?这个问题其实没有标准答案的,这里附上一些常用的做法。

代码如下:

# -*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException
from selenium.common.exceptions import NoSuchElementException
from config.sysconfig import *

import sys
import time

class BaseOperate():
    # 声明一个webdriver类
    operate_driver = webdriver.Chrome

    def __init__(self, driver):
        # 构造函数里将driver传入
        self.operate_driver = driver

    def find_element(self, *loc):
        """
        重封装的find方法,接受元祖类型的参数,默认等待元素5秒,寻找失败时自动截图
        :param loc:元组类型,必须是(By.NAME, 'username')这样的结构
        :return:元素对象webelement
        """
        try:
            webelement = WebDriverWait(self.operate_driver, 5).until(lambda x: x.find_element(*loc))
            return webelement
        except (TimeoutException, NoSuchElementException) as e:
            #寻找失败时自动截图至指定目录sreenshot,截图名称为调用方法名(测试用例名)+ 时间戳 + png后缀
            self.operate_driver.get_screenshot_as_file(SCREENSHOTURL + sys._getframe(1).f_code.co_name + time.strftime(ISOTIMEFORMAT, time.localtime(time.time()))+".png")

    def click_element(self, *loc):
        """
        重封装的click方法,将寻找和点击封装到一起,适用于点击次数不多的元素
        :param loc:元组类型,必须是(By.NAME, 'username')这样的结构
        :return:None
        """
        try:
            webelement = self.find_element(*loc)
            webelement.click()
        except (TimeoutException, NoSuchElementException) as e:
            print ('Error details :%s' % (e.msg))

    def is_page_has_text(self, text):
        """
        判断当前页面是否存在指定的文字
        :param text:字符串类型,要判断是否存在的文字
        :return:布尔值,True代表存在,False代表不存在
        """
        nowtime = time.time()
        while self.operate_driver.page_source.find(text) < 0:
            time.sleep(2)
            if time.time() - nowtime >= 30000:
                return False
        return True

    def switch_to_last_handles(self):
        """
        在打开的窗口里选择最后一个
        :return:None
        """
        all_handles = self.operate_driver.window_handles
        self.operate_driver.switch_to_window(all_handles[-1])

    def switch_to_another_hanles(self, now_handle):
        """
        只适用于打开两个窗口的情况,传入现在的窗口句柄后,选择另一个窗口
        :param now_handle:现在的窗口句柄
        :return:
        """
        # 得到当前开启的所有窗口的句柄
        all_handles = self.operate_driver.window_handles 
        # 获取到与当前窗口不一样的窗口
        for handle in all_handles:
            if handle != now_handle:  
                self.operate_driver.switch_to_window(handle)

    def clear_and_sendkeys(self, sendtexts, *loc):
        """
        先清除当前文本框内的文字再输入新的文字的方法
        :param sendtexts:要输入的新的文字
        :param loc:元组类型,必须是(By.NAME, 'username')这样的结构
        :return:None
        """
        try:
            webelement = self.find_element(*loc)
            webelement.clear()
            webelement.send_keys(sendtexts)
        except (TimeoutException, NoSuchElementException) as e:
            print ('Error details :%s' % (e.msg))

这里重点说一下find的封装思路,之前说过,我们希望元素的位置和主代码分离,因此这里就用了selenium的find方法的另一种形式,

find_element (by ='id',value = None )

这样的结构,我们把元素按照ELEMENT = {'登录按钮': (By.ID, 'loginSub')},这样的结构存储在我们预定的config位置里,文件名就叫他emtconfig,之后元素的增加修改删除都在这里维护,当需要用到该元素的时候,就引入这个文件,例如这段代码:

# -*- coding: utf-8 -*-
from maincase.maincase import MainCase
from config.emtconfig import ELEMENT

class Test(MainCase):
    def test_1(self):
        qmarket_loginbtn = self.base_operate.find_element(*ELEMENT['QQ市场首页登录按钮'])

用于参考的emtconfig的内容:

# -*- coding: utf-8 -*-
from selenium.webdriver.common.by import By

# 页面元素
ELEMENT = {
    #首页元素
    'QQ市场首页登录按钮': (By.XPATH, "//*[@id='jmod_topbar']//*/a[text()='登录']"),
    '选择使用账号登录按钮': (By.XPATH, "//*[@id='switcher_plogin']"),
    
    #登录界面元素
    'QQ号文本框': (By.ID, "u"),
    'QQ密码文本框': (By.ID, "p"),
    'QQ登录按钮': (By.ID, "login_button"),
    
    #管理中心元素
    '首页管理中心按钮': (By.XPATH, "//*[@id='jmod_topbar']//*/a[text()='管理中心']"),
    '应用管理中心更新安装包按钮': (By.XPATH, "/html/body//*/a[text()='更新安装包']"),
    
    #应用详情页元素
    '点击上传按钮': (By.XPATH, "//*[@id='j-apk-box']//*/p[text()='更新安装包']"),
    '应用更新说明文本框': (By.XPATH, "//*[@id='j-apk-box']//*/textarea"),
    '提交审核按钮': (By.ID, "j-submit-btn"),
    '确认提交审核': (By.ID, "j-confirm-yes"),
}

为了知道为什么元素不能被寻找到,在用例执行的时候到底发生了什么,所以我们希望当寻找失败时可以有个截图方便回溯当时的环境状况,因此我们用了py的异常机制,在寻找失败时,执行except下的方法,也就是截图方法,我在config的位置新建了一个sysconfig的文件,里面用于存放一些系统的信息,例如截图的位置或者全局样式等等,我们的截图的方法就用了这个位置。

sysconfig现在的内容:

# -*- coding: utf-8 -*-

# 截图路保存径,绝对路径,也可以用相对路径
SCREENSHOTURL = 'C:/Users/zyj/PycharmProjects/uitest/sreenshot/'

# 时间样式
ISOTIMEFORMAT = '%Y%m%d%H%M%S'

设计逻辑操作类

这个是用于封装一些常用的逻辑操作或者跳转路径的操作的类,预留的位置operate里,名字叫做BusinessOperate。因为是和业务强相关,所以就不举例了,大约的实现方法是:

# -*- coding: utf-8 -*-
from operate.baseoperate import BaseOperate

class BusinessOperate(BaseOperate):
    pass

因为没有实际方法,所以直接pass了,诸位真正用的时候可以自己根据业务需要补充代码。

设计工具类

这个类主要是非selenium的内容,但是用例也会用到的操作,例如打开windows窗口选择文件上传,发送email等,位置位于util里,这里上前文提到的两个实例。
封装打开win窗口的操作,类名为W32Operate:

# -*- coding: utf-8 -*-

import win32gui
import win32con
import sys #要重新载入sys。因为 Python 初始化后会删除 sys.setdefaultencoding 这个方法

reload(sys)
sys.setdefaultencoding('utf-8')
class W32Operate:

   def win_upload(self, filepath):
       """
       上传操作时,打开win的上传弹框,选取指定的文件
       :param filepath: 上传的文件的路径
       :return: None
       """
       dialog = win32gui.FindWindow('#32770', u'打开')  # 对话框
       print (dialog)
       #win32gui.SetForegroundWindow(dialog)
       ComboBoxEx32 = win32gui.FindWindowEx(dialog, 0, 'ComboBoxEx32', None)
       ComboBox = win32gui.FindWindowEx(ComboBoxEx32, 0, 'ComboBox', None)
       Edit = win32gui.FindWindowEx(ComboBox, 0, 'Edit', None)  # 上面三句依次寻找对象,直到找到输入框Edit对象的句柄
       button = win32gui.FindWindowEx(dialog, 0, 'Button', u'打开(&O)')  # 确定按钮Button
       win32gui.SendMessage(Edit, win32con.WM_SETTEXT, None, filepath)  # 往输入框输入绝对地址
       win32gui.SendMessage(dialog, win32con.WM_COMMAND, 1, button)  # 按button

封装email的操作,类名为EmailOperate(smtp的信息写在了sysconfig里,另外,如果你是采用我之后提到的pytest+allure+jenkins的方法的话,邮件的发送无需在框架内实现,这里写一下仅供参考):

# -*- coding: utf-8 -*-
from email.mime.text import MIMEText
from email.header import Header
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email import encoders
from config.sysconfig import *

import smtplib

class EmailOperate:

    def send_mail_with_file(self, filepath, filename, subject):
        """
        发送一个带有附件(测试报告)的邮件,邮件的配置项来自sysconfig
        :param filepath: 报告文件的路径
        :param filename: 报告文件的名称
        :param subject: 邮件标题
        :return: None
        """
        # 邮件对象:
        msg = MIMEMultipart()
        msg['From'] = "UIAutoTester"
        msg['To'] = "you"
        msg['Subject'] = Header(subject, 'utf-8').encode()

        # 邮件正文是MIMEText:
        msg.attach(MIMEText('请查收自动化测试结果,见附件', 'plain', 'utf-8'))

        # 添加附件:
        with open(filepath, 'rb') as f:
            # 设置附件的MIME和文件名:
            mime = MIMEApplication(open(filepath, 'rb').read())
            # 加上必要的头信息:
            mime.add_header('Content-Disposition', 'attachment', filename=filename)
            mime.add_header('Content-ID', '<0>')
            mime.add_header('X-Attachment-Id', '0')
            # 把附件的内容读进来:
            mime.set_payload(f.read())
            # 用Base64编码:
            encoders.encode_base64(mime)
            # 添加到MIMEMultipart:
            msg.attach(mime)

        server = smtplib.SMTP(smtp_server, 587)
        server.starttls()
        server.set_debuglevel(1)
        server.login(from_addr, password)
        server.sendmail(from_addr, [to_addr], msg.as_string())
        server.quit()

用例的组织和管理

我们之前预留了testsuite的位置,现在我们可以在这个位置新建我们自定义的测试用例集,使用unittest框架的话,用例的组织可以用三种方法,addTest(添加具体测试用例至用例集),makeSuite(加载某个类下的所有测试用例至用例集),以及discover(加载某个路径下的所有类里的所有测试用例至用例集),三种方法的示范代码如下:
addTest:

# coding = utf-8  
import unittest  

suite = unittest.TestSuite()  
suite.addTest(你的测试用例类名('该类名下的测试用例方法名'))  
  
if __name__=='__main__':  
    #执行用例  
    runner=unittest.TextTestRunner()  
    runner.run(suite)  

makeSuite:

# coding = utf-8  
import unittest  
  
suite = unittest.TestSuite(unittest.makeSuite(你的测试用例类名))  
  
if __name__=='__main__':  
    #执行用例  
    runner=unittest.TextTestRunner()  
    runner.run(suite)  

discover:

# coding = utf-8  
import unittest  
  
suite = unittest.TestLoader().discover("你的测试用例类的路径")  
  
  
if __name__=='__main__':  
    #执行用例  
    runner=unittest.TextTestRunner()  
    runner.run(suite)  
生成测试报告

传统的和unittest框架配套的报告模块是HTMLTestRunner,使用方法也很简单,首先是下载,把下载的文件放到...\Python27\Lib目录下,然后用的时候import一下就行。
我们可以把代码写到testsuite文件里的最下面,报告的位置就用我们预留的testreport位置,例如:

if __name__=='__main__':
    #执行测试
    filename = config.ReportUrl + time.strftime(config.ISOTIMEFORMAT, time.localtime(time.time())) + 'result.html'
    fp = file(filename, 'wb')

    runner = HTMLTestRunner.HTMLTestRunner(
        stream=fp,
        title='YCG Test Report',
        description='YcgkfsApp All Testcase'
        )

    runner.run(suite)

这样执行完了testsuite之后,就会在testreport目录下生成一个html文件格式的报告,结合之前写的email方法,就可以把报告发出去了,这一段就不举例了,因为后面会介绍一个更加简便的方法去做这件事。
生成的报告界面如下:

img_379372ca085ef835b70905070482d69e.png
image

目录
相关文章
|
3天前
|
Web App开发 JavaScript 前端开发
深入理解自动化测试框架Selenium的设计与实现
【4月更文挑战第20天】 在软件测试领域,自动化测试已成为提升测试效率和确保产品质量的关键手段。Selenium作为一款广泛使用的开源自动化测试框架,其设计精巧且功能强大,为Web应用提供了一种灵活、高效的测试解决方案。本文将深入探讨Selenium的核心架构与实现细节,解析其如何通过模拟用户操作来执行测试用例,以及它如何适应不断变化的Web技术标准。通过对Selenium内部机制的剖析,旨在帮助测试工程师更好地掌握该工具,并在测试实践中发挥其最大效能。
|
4天前
|
前端开发 JavaScript Java
《手把手教你》系列技巧篇(二十五)-java+ selenium自动化测试-FluentWait(详细教程)
【4月更文挑战第17天】其实今天介绍也讲解的也是一种等待的方法,有些童鞋或者小伙伴们会问宏哥,这也是一种等待方法,为什么不在上一篇文章中竹筒倒豆子一股脑的全部说完,反而又在这里单独写了一篇。那是因为这个比较重要,所以宏哥专门为她量身定制了一篇。FluentWait是Selenium中功能强大的一种等待方式,翻译成中文是流畅等待的意思。在介绍FluentWait之前,我们来讨论下为什么需要设置等待,我们前面介绍了隐式等待和显式等待。
27 3
|
6天前
|
Java 测试技术 定位技术
《手把手教你》系列技巧篇(二十三)-java+ selenium自动化测试-webdriver处理浏览器多窗口切换下卷(详细教程)
【4月更文挑战第15天】本文介绍了如何使用Selenium进行浏览器窗口切换以操作不同页面元素。首先,获取浏览器窗口句柄有两种方法:获取所有窗口句柄的集合和获取当前窗口句柄。然后,通过`switchTo().window()`方法切换到目标窗口句柄。在项目实战部分,给出了一个示例,展示了在百度首页、新闻页面和地图页面之间切换并输入文字的操作。最后,文章还探讨了在某些情况下可能出现的问题,并提供了一个简单的本地HTML页面示例来演示窗口切换的正确操作。
26 0
|
7天前
|
敏捷开发 监控 前端开发
深入理解自动化测试框架Selenium的架构与实践
【4月更文挑战第16天】 在现代软件开发过程中,自动化测试已成为确保产品质量和加快迭代速度的关键手段。Selenium作为一种广泛使用的自动化测试工具,其开源、跨平台的特性使得它成为业界的首选之一。本文旨在剖析Selenium的核心架构,并结合实际案例探讨其在复杂Web应用测试中的高效实践方法。通过详细解读Selenium组件间的交互机制以及如何优化测试脚本,我们希望为读者提供深入理解Selenium并有效运用于日常测试工作的参考。
13 1
|
7天前
|
自然语言处理 测试技术 API
深入理解自动化测试框架Selenium的设计理念与实践
【4月更文挑战第15天】 在现代软件开发过程中,自动化测试已成为确保产品质量和加速迭代的关键手段。Selenium作为一种广泛使用的自动化测试框架,提供了对多种浏览器和平台的支持,极大地促进了Web应用的功能测试。本文旨在剖析Selenium的核心设计理念,探讨其在实际项目中的应用,并指出常见的误区及最佳实践,以期帮助测试工程师更高效地利用Selenium进行测试工作。
|
1月前
|
前端开发 搜索推荐 开发者
SAP UI5 sap.m.Column 控件的 minScreenWidth 属性介绍
SAP UI5 sap.m.Column 控件的 minScreenWidth 属性介绍
27 0
|
1月前
|
JavaScript 前端开发 开发者
SAP UI5 控件 sap.m.ListBase 的 inset 属性的作用介绍
SAP UI5 控件 sap.m.ListBase 的 inset 属性的作用介绍
15 0
|
1月前
|
前端开发 JavaScript API
SAP UI5 sap.ui.require.toUrl 的作用介绍
SAP UI5 sap.ui.require.toUrl 的作用介绍
28 0
|
1月前
|
JSON 前端开发 测试技术
SAP UI5 sap.ui.core.util.MockServer.simulate 方法介绍
SAP UI5 sap.ui.core.util.MockServer.simulate 方法介绍
21 0
|
1月前
使用 SAP UI5 Event Bus 机制,修复 SAP UI5 分页显示数据的一个 bug 试读版
使用 SAP UI5 Event Bus 机制,修复 SAP UI5 分页显示数据的一个 bug 试读版
20 0

热门文章

最新文章