1. 程式人生 > >Selenium自動化測試Python五:WebDriver設計模式

Selenium自動化測試Python五:WebDriver設計模式

WebDriver 設計模式

歡迎閱讀WebDriver進階講義。本篇講義將會重點介紹Selenium WebDriver 自動化框架的設計,著重使用Page Object設計模式,以及使用HTML測試報告和整合測試報告並自動傳送郵件。

Page Object 設計模式

在討論設計模式之前,我們首先接著上一個講義,來討論自動化測試框架。

什麼是框架?

框架(Framework)是整個或部分系統的可重用設計,表現為一組抽象構件及構件例項間互動的方法;另一種定義認為,框架是可被應用開發者定製的應用骨架。前者是從應用方面而後者是從目的方面給出的定義。可以說,一個框架是一個可複用的設計構件,它規定了應用的體系結構,闡明瞭整個設計、協作構件之間的依賴關係、責任分配和控制流程,表現為一組抽象類以及其例項之間協作的方法,它為構件複用提供了上下文(Context)關係。因此構件庫的大規模重用也需要框架。其實目前為止,框架還沒有統一定義,我比較喜歡Ralph Johnson所給出的定義:

一個框架是一個可複用設計,它是由一組抽象類及其例項間協作關係來表達的 【Johnson 98】。這個定義是從框架內涵的角度來定義框架的,當然也可以從框架用途的角度來給出框架的定義:一個框架是在一個給定的問題領域內,一個應用程式的一部分設計與實現【Bosch 97】。

為什麼要用框架?

又是一個理所當然的問題。因為軟體系統發展到今天已經很複雜了,特別是伺服器端軟體,涉及到的知識,內容,問題太多。在某些方面使用別人成熟的框架,就相當於讓別人幫你完成一些基礎工作,你只需要集中精力完成系統的業務邏輯設計。而且框架一般是成熟,穩健的,他可以處理系統很多細節問題,比如,事物處理,安全性,資料流控制等問題。還有框架一般都經過很多人使用,所以結構很好,所以擴充套件性也很好,而且它是不斷升級的,你可以直接享受別人升級程式碼帶來的好處。

為什麼要搭建自動化測試框架?

從前我以為,自動化測試最重要的事情是找物件(To Find Test Object)。現在我明白了一個道理,沒有框架的自動化測試是找不到物件的,即使找到了也不會幸福的。就跟現實中,沒有車沒有房的人是很難找到物件的一個道理。

自動化測試的開發,通常是由自動化測試的需求決定的。這個需求主要包括:

  • 自動化測試更便於實施。這個說的是,你寫測試指令碼要方便。一個好的自動化測試框架是可以讓不那麼懂技術的人也可以寫自動化測試指令碼的,哼。
  • 解決自動化測試指令碼本身存在的問題,如異常處理和場景恢復。
  • 測試易於維護。自動化測試專案,基本都是沒有好的管理以及維護,一定是個大坑。我可以很負責地說,自動化測試沒有一年半載,你是看不到產出的。所以管理及維護就成了最重要的事情。而好的框架,可以減少你在管理維護中所投入的人力物力精力。
  • 可重用性。框架的意義之一就在於可重用吧。所以在框架裡,你可以實現一些通用功能,簡化指令碼開發過程。
  • 美觀易讀的測試報告。拿Selenium來說,它產出的測試報告只是基於測試指令碼的,並沒有那種基於測試集的報告,所以如果你要,測試框架裡可以實現。

還有很多測試需求,我沒辦法一一列舉出來,多數需求我們都可以在測試框架裡去定製。現在可以回答上面那個問題了,record & playback是不會幸福的,你需要自動化測試框架。

請慎重考慮是否需要自動化測試

自動化測試的特點(成本投入高,風險大)

自動化測試是個很傲嬌的東西,它很挑專案。首先專案週期要長,但是需求不會頻繁變更;其次系統中多數物件要可以被識別,並且不存在大量第三方外掛。而且你要清楚,你不能指望自動化測試去幫你發現新的bug,自動化測試本身是不具備想象力的(相對於手工測試)。它的優勢在於反覆迭代,它的價值產出在於長期的迴歸測試,以保證被測產品長期穩定地版本更新。

關於自動化測試的切入點,通常要在完整的系統測試之後才算具備引入自動化測試的基本條件。

目前我所做的自動化測試成功案例,無一不具備良好的管理和優良的測試框架。二者缺一,自動化測試必然成為大坑。填坑的成本,很高。

Page Object 設計原理

Page Object設計模式是Selenium自動化測試專案的最佳設計模式之一,強調測試、邏輯、資料和驅動相互分離。

Page Object模式是Selenium中的一種測試設計模式,主要是將每一個頁面設計為一個Class,其中包含頁面中需要測試的元素(按鈕,輸入框,標題等),這樣在Selenium測試頁面中可以通過呼叫頁面類來獲取頁面元素,這樣巧妙的避免了當頁面元素id或者位置變化時,需要改測試頁面程式碼的情況。當頁面元素id變化時,只需要更改測試頁Class中頁面的屬性即可。

它的好處如下:

  • 集中管理元素物件
  • 集中管理一個page內的公共方法
  • 後期維護方便

Page Object 的物件

  1. WebDriver封裝
    • 這裡是對Selenium的封裝,完成封裝以後的基本封裝程式碼。
  2. Page 基類
    • 設計了一個基本的Page類,以便所有的頁面進行繼承,該類標明瞭一個sub page類的基本功能和公共的功能。
  3. Sub Pages(s)子類
    • 具體的頁面的類,定義了某個具體的頁面的功能。
  4. Tests 類
    • 這部分描述的是具體的測試用例。
  5. 定義Test Suite
    • 多個測試用例新增在一個Test套件裡面,一起執行。
  6. 定義Test Runner
    • 設計測試的Runner,開啟整個測試,並且對測試的結果生成HTML測試報告,並通過郵件傳送到指定郵箱。
  7. 定義測試的主入口
    • 定義測試的主要入口類,程式碼的入口

HTML測試報告

HTML測試報告需要引入HTMLTestRunner

from ranzhiWeekend import HTMLTestRunner

HTMLTestRunner是基於Python2.7的,我們的課程講義基於Python3.x,那麼需要對這個檔案做一定的修改。

測試的示例程式碼如下

        # 宣告一個測試套件
        suite = unittest.TestSuite()
        # 新增測試用例到測試套件
        suite.addTest(RanzhiTests("test_ranzhi_login"))

        # 建立一個新的測試結果檔案
        buf = open("./result.html", "wb")

        # 宣告測試執行的物件
        runner = HTMLTestRunner.HTMLTestRunner(stream=buf,
                                               title="Ranzhi Test Result",
                                               description="Test Case Run Result")
        # 執行測試,並且將結果生成為HTML
        runner.run(suite)

        # 關閉檔案輸出
        buf.close()

整合測試報告

使用郵件傳送HTML測試報告的指令碼如下

        # 開啟測試報告結果
        f = open("./result.html", "rb")

        # 將測試結果放到郵件的主體中
        mailBody = f.read()
        # 關閉測試結果的檔案
        f.close()

        # 宣告一個郵件物件,用剛剛得到的郵件主體
        msg = MIMEText(mailBody, "html", "utf-8")
        # 設定郵件的主題
        msg["subject"] = Header("Automation Test Result", "utf-8")

        # 建立一個SMTP服務物件
        # simple message transfer protocol
        # 簡單的訊息轉移協議
        smtpMail = smtplib.SMTP()

        # 連線SMTP的伺服器
        smtpMail.connect("mail.51testing.com")

        # 登入SMTP的伺服器
        smtpMail.login("[email protected]", "123456789")

        # 使用SMTP的伺服器傳送郵件
        smtpMail.sendmail("[email protected]", targetEmail, msg.as_string())

        # 退出SMTP物件
        smtpMail.quit()

自動化測試框架示例

WebDriver封裝

# coding=utf-8
from selenium import webdriver
from selenium.webdriver.support.select import Select


class AutomateDriver(object):
    """
    a simple demo of selenium framework tool
    """

    def __init__(self):

        driver = webdriver.Firefox()
        try:
            self.driver = driver
        except Exception:
            raise NameError("Firefox Not Found!")


    def clearCookies(self):
        """
        clear all cookies after driver init
        """
        self.driver.delete_all_cookies()

    def refreshBrowser(self):
        self.driver.refresh()

    def maximizeWindow(self):
        self.driver.maximize_window()

    def navigate(self, url):
        self.driver.get(url)

    def quitBrowser(self):
        self.driver.quit()

    def closeBrowser(self):
        self.driver.close()

    def getElement(self, selector):
        """
        to locate element by selector
        :arg
        selector should be passed by an example with "i,xxx"
        "x,//*[@id='langs']/button"
        :returns
        DOM element
        """
        if ',' not in selector:
            return self.driver.find_element_by_id(selector)
        selector_by = selector.split(',')[0]
        selector_value = selector.split(',')[1]

        if selector_by == "i" or selector_by == 'id':
            element = self.driver.find_element_by_id(selector_value)
        elif selector_by == "n" or selector_by == 'name':
            element = self.driver.find_element_by_name(selector_value)
        elif selector_by == "c" or selector_by == 'class_name':
            element = self.driver.find_element_by_class_name(selector_value)
        elif selector_by == "l" or selector_by == 'link_text':
            element = self.driver.find_element_by_link_text(selector_value)
        elif selector_by == "p" or selector_by == 'partial_link_text':
            element = self.driver.find_element_by_partial_link_text(selector_value)
        elif selector_by == "t" or selector_by == 'tag_name':
            element = self.driver.find_element_by_tag_name(selector_value)
        elif selector_by == "x" or selector_by == 'xpath':
            element = self.driver.find_element_by_xpath(selector_value)
        elif selector_by == "s" or selector_by == 'selector_selector':
            element = self.driver.find_element_by_css_selector(selector_value)
        else:
            raise NameError("Please enter a valid type of targeting elements.")

        return element

    def type(self, selector, text):
        """
        Operation input box.

        Usage:
        driver.type("i,el","selenium")
        """
        el = self.getElement(selector)
        el.clear()
        el.send_keys(text)

    def click(self, selector):
        """
        It can click any text / image can be clicked
        Connection, check box, radio buttons, and even drop-down box etc..

        Usage:
        driver.click("i,el")
        """
        el = self.getElement(selector)
        el.click()

    def selectByIndex(self, selector, index):
        """
        It can click any text / image can be clicked
        Connection, check box, radio buttons, and even drop-down box etc..

        Usage:
        driver.select_by_index("i,el")
        """
        el = self.getElement(selector)
        Select(el).select_by_index(index)

    def clickByText(self, text):
        """
        Click the element by the link text

        Usage:
        driver.click_text("新聞")
        """
        self.getElement('p,' + text).click()

    def submit(self, selector):
        """
        Submit the specified form.

        Usage:
        driver.submit("i,el")
        """
        el = self.getElement(selector)
        el.submit()

    def executeJs(self, script):
        """
        Execute JavaScript scripts.

        Usage:
        driver.js("window.scrollTo(200,1000);")
        """
        self.driver.execute_script(script)

    def getAttribute(self, selector, attribute):
        """
        Gets the value of an element attribute.

        Usage:
        driver.get_attribute("i,el","type")
        """
        el = self.getElement(selector)
        return el.getAttribute(attribute)

    def getText(self, selector):
        """
        Get element text information.

        Usage:
        driver.get_text("i,el")
        """
        el = self.getElement(selector)
        return el.text

    def getDisplay(self, selector):
        """
        Gets the element to display,The return result is true or false.

        Usage:
        driver.get_display("i,el")
        """
        el = self.getElement(selector)
        return el.is_displayed()

    def getTitle(self):
        '''
        Get window title.

        Usage:
        driver.get_title()
        '''
        return self.driver.title

    def getUrl(self):
        """
        Get the URL address of the current page.

        Usage:
        driver.get_url()
        """
        return self.driver.current_url

    def acceptAlert(self):
        '''
            Accept warning box.

            Usage:
            driver.accept_alert()
            '''
        self.driver.switch_to.alert.accept()

    def dismissAlert(self):
        '''
        Dismisses the alert available.

        Usage:
        driver.dismissAlert()
        '''
        self.driver.switch_to.alert.dismiss()

    def implicitlyWait(self, secs):
        """
        Implicitly wait. All elements on the page.

        Usage:
        driver.implicitly_wait(10)
        """
        self.driver.implicitly_wait(secs)

    def switchFrame(self, selector):
        """
        Switch to the specified frame.

        Usage:
        driver.switch_to_frame("i,el")
        """
        el = self.getElement(selector)
        self.driver.switch_to.frame(el)

    def switchDefaultFrame(self):
        """
        Returns the current form machine form at the next higher level.
        Corresponding relationship with switch_to_frame () method.

        Usage:
        driver.switch_to_frame_out()
        """
        self.driver.switch_to.default_content()

    def openNewWindow(self, selector):
        '''
        Open the new window and switch the handle to the newly opened window.

        Usage:
        driver.open_new_window()
        '''
        original_windows = self.driver.current_window_handle
        el = self.getElement(selector)
        el.click()
        all_handles = self.driver.window_handles
        for handle in all_handles:
            if handle != original_windows:
                self.driver._switch_to.window(handle)

Base Page類

class RanzhiBasePage():
    def __init__(self, driver, baseUrl):
        """
        構造方法
        :param driver: 封裝好的webdriver
        :param baseUrl: 然之系統的基本url http://【localhost:808】/ranzhi/www
        """

        self.baseUrl = baseUrl
        self.driver = driver

    def openPage(self, url):
        """
        開啟然之系統的頁面,通過拼接URL的方式
        :param url: /sys/index.html
        :return:
        """
        self.driver.navigate(self.baseUrl + url)

Sub Page類

from ranzhiWeekend.ranzhi_base_page import RanzhiBasePage


class RanzhiSubLoginPage(RanzhiBasePage):
    def __init__(self, driver, baseUrl):
        """

        :param driver:
        :param baseUrl:
        """
        # 呼叫其 基類 RanzhiBasePage的 建構函式
        # 實現 基類 的建構函式的功能
        super().__init__(driver, baseUrl)
        self.loginPageUrl = "/sys/user-login.html"
        self.mainPageUrl = "/sys/index.html"
        self.driver.clearCookies()

    def login(self, userName, password):
        self.openPage(self.loginPageUrl)
        # self.driver.clearCookies()
        self.driver.implicitlyWait(5)
        self.driver.type("account", userName)
        self.driver.type("password", password)
        self.driver.click("submit")

    def getMainPage(self):
        return self.baseUrl + self.mainPageUrl

Tests Case 類

import unittest
from time import sleep

from ranzhiWeekend.automate_driver import AutomateDriver
from ranzhiWeekend.ranzhi_sub_login_page import RanzhiSubLoginPage

"""
1. 匯入 unittest
2. 繼承 unittest.TestCase
3. 寫用例 方法以 test 開頭
4. 考慮使用 setUp() 和 tearDown()
"""


class RanzhiTests(unittest.TestCase):
    def setUp(self):
        """
        開始每個測試前的準備事項
        :return:
        """
        self.autoDriver = AutomateDriver()
        self.baseUrl = "http://localhost:808/ranzhi/www"

    def tearDown(self):
        """
        結束每個測試後的清理工作
        :return:
        """
        self.autoDriver.quitBrowser()

    def test_ranzhi_login(self):
        """
        測試用例:測試然之登入
        :return:
        """
        # 新建然之的頁面物件
        loginPage = RanzhiSubLoginPage(self.autoDriver, self.baseUrl)

        # 利用然之的頁面物件進行登入
        loginPage.login("admin", "admin")
        sleep(2)
        # 斷言 是否登入成功
        self.assertEqual(loginPage.getMainPage(), self.autoDriver.getUrl(), u"登入失敗")

Tests Runner類

import smtplib
import unittest
from email.header import Header
from email.mime.text import MIMEText

from ranzhiWeekend import HTMLTestRunner
from ranzhiWeekend.ranzhi_tests_0605 import RanzhiTests


class RanzhiTestRunner():

    def runTest(self):
        """
        執行測試用例
        :return:
        """

        # 宣告一個測試套件
        suite = unittest.TestSuite()
        # 新增測試用例到測試套件
        suite.addTest(RanzhiTests("test_ranzhi_login"))

        # 建立一個新的測試結果檔案
        buf = open("./result.html", "wb")

        # 宣告測試執行的物件
        runner = HTMLTestRunner.HTMLTestRunner(stream=buf,
                                               title="Ranzhi Test Result",
                                               description="Test Case Run Result")
        # 執行測試,並且將結果生成為HTML
        runner.run(suite)

        # 關閉檔案輸出
        buf.close()

    def sendEmail(self, targetEmail):
        """
        傳送郵件
        :param targetEmail:
        :return:
        """

        # 開啟測試報告結果
        f = open("./result.html", "rb")

        # 將測試結果放到郵件的主體中
        mailBody = f.read()
        # 關閉測試結果的檔案
        f.close()

        # 宣告一個郵件物件,用剛剛得到的郵件主體
        msg = MIMEText(mailBody, "html", "utf-8")
        # 設定郵件的主題
        msg["subject"] = Header("Automation Test Result", "utf-8")

        # 建立一個SMTP服務物件
        # simple message transfer protocol
        # 簡單的訊息轉移協議
        smtpMail = smtplib.SMTP()

        # 連線SMTP的伺服器
        smtpMail.connect(“***.******.com")

        # 登入SMTP的伺服器
        smtpMail.login(“*******@*****.com", “*********")

        # 使用SMTP的伺服器傳送郵件
        smtpMail.sendmail(“*******@********.com", targetEmail, msg.as_string())

        # 退出SMTP物件
        smtpMail.quit()

main函式入口

if __name__ == "__main__":
    # 例項化一個runner
    runner = RanzhiTestRunner()

    # 執行測試
    runner.runTest()

    # 傳送測試結果
    runner.sendEmail(“********@******.com")

轉載來源:http://www.jianshu.com/p/b5957c487350