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

三、填充我们的框架

设计主类

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

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

上一篇:Silverlight:Dependency Property(依赖属性)学习笔记


下一篇:文件管理基础命令之二