美文网首页浅谈自动化测试我爱编程
从零开始搭建一个简单的ui自动化测试框架03(pytest+se

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

作者: Null_ice | 来源:发表于2017-11-24 15:19 被阅读671次

    三、填充我们的框架

    设计主类

    我们首先来实现我们的测试用例的主类设计,这个类主要用以被其他的测试用例继承,来实现一些每个测试用例都会做的事情,具体一点就是:
    继承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方法,就可以把报告发出去了,这一段就不举例了,因为后面会介绍一个更加简便的方法去做这件事。
    生成的报告界面如下:

    image

    相关文章

      网友评论

        本文标题:从零开始搭建一个简单的ui自动化测试框架03(pytest+se

        本文链接:https://www.haomeiwen.com/subject/bwhfbxtx.html