天天看點

從零開始搭建一個簡單的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

下一篇: 代理模式