Selenium3與Python3實戰 Web自動化測試框架(二)


Selenium3與Python3實戰 Web自動化測試框架


 

一、項目實戰中PO模型的設計與封裝

 一般將所有的元素、數據都放在代碼中,並不利於自動化代碼的維護。最好的方式是盡量把數據、頁面、操作進行分離開:PO設計模式

 PO設計模式的優勢:

  1. PO提供了一種業務流程與頁面元素操作分離的模式,這使得測試代碼變得更加清晰。
  2. 頁面對象與用例分離,使得我們更好的復用對象。
  3. 可復用的頁面方法代碼會變得更加優化
  4. 更加有效的命名方式使得我們更加清晰的知道方法所操作的UI元素

1、使用PO模式實現注冊頁面封裝

1)關於配置文件

 LocalElement.py:

[RegisterElement]
user_email=id>register_email
user_email_error=id>register_email-error
user_name=id>register_nickname
user_name_error=id>register_nickname-error
password=id>register_password
password_error=id>register_password-error
code_image=id>getcode_num
code_text=id>captcha_code
code_text_error=id>captcha_code-error
register_button=id>register-btn

 setting.py:

import os

base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 項目首路徑
codeerror_path = os.path.join(base_dir,'Image','codeerror.png')  # 驗證碼錯誤圖片路徑
code_path = os.path.join(base_dir,'Image','code.png')  # 驗證碼圖片保存路徑
config_ini_dir = os.path.join(base_dir,'setting','localElement.ini')  # localElement.ini 配置文件路徑

 

2)read.ini.py :用於讀取LocalElement.py文件中的配置信息

from setting.setting import config_ini_dir
import configparser

class Read_Ini(object): # 初始化
    def __init__(self,node = None):
        if node:
            self.node = node
        else:
            self.node = 'RegisterElement'  # 配置文件中的某個節點
        self.cf = self.load_ini()

    def load_ini(self):  # 加載文件
        cf = configparser.ConfigParser()  # 使用 configparser模塊讀取配置文件信息
        cf.read(config_ini_dir)  # 配置文件所在路徑
        return cf

    def get_value(self,key): # 獲取配置文件中key的value值
        data = self.cf.get(self.node,key)
        return data

 

3)base包下新建find_element.py文件:通過read_ini.py文件獲取配置文件的信息,用於定位注冊頁面的目標元素

from util.read_ini import Read_Ini
class FindElement(object):
    """獲取元素所在位置"""
    def __init__(self,driver):
        self.driver = driver
    def get_element(self,key):
        read_ini = Read_Ini()
        data = read_ini.get_value(key)
        by,value = data.split('>')

        try:
            if by == 'id':
                return self.driver.find_element_by_id(value)
            elif by == 'name':
                return self.driver.find_element_by_name(value)
            elif by == 'className':
                return self.driver.find_element_by_class_name(value)
            elif by == 'xpath':
                return self.driver.find_element_by_xpath(value)
            else:
                return self.driver.find_element_by_css(value)
        except Exception as e:
            # print("find_element錯誤信息:",e)
            return None

 

4)page包新建register_page.py文件: 通過find_element.py文件,獲取目標元素所在的位置

from base.find_element import FindElement
class RegisterPage(object):
    """獲取元素所在位置"""
    def __init__(self,driver):
        self.fd = FindElement(driver)
    
    #獲取郵箱元素
    def get_email_element(self):
        return self.fd.get_element("user_email")

    #獲取用戶名元素
    def get_username_element(self):
        return self.fd.get_element("user_name")
    #獲取密碼元素
    def get_password_element(self):
        return self.fd.get_element("password")
    #獲取驗證碼元素
    def get_code_element(self):
        return self.fd.get_element("code_text")
    #獲取注冊按鈕元素
    def get_button_element(self):
        return self.fd.get_element("register_button")
    #獲取郵箱錯誤元素
    def get_email_error_element(self):
        return self.fd.get_element("user_email_error")
    #獲取用戶名錯誤元素
    def get_name_error_element(self):
        return self.fd.get_element("user_name_error")
    #獲取密碼錯誤元素
    def get_password_error_element(self):
        return self.fd.get_element("password_error")
    #獲取驗證碼錯誤元素
    def get_code_error_element(self):
        return self.fd.get_element("code_text_error")

 

5)handle包下新建register_handle.py文件:結合register_page.py文件獲取目標元素位置,再自動輸入對應信息

#coding=utf-8
from page.register_page import RegisterPage
from util.get_code_value import GetCode

class RegisterHandle(object):
    """打開頁面后自動輸入相應信息"""
    def __init__(self,driver):
        self.driver = driver
        self.register_p = RegisterPage(self.driver)

    #輸入郵箱
    def send_user_email(self,email):
        # self.loger.info("輸入的郵箱值是:"+email)
        self.register_p.get_email_element().send_keys(email)
    
    #輸入用戶名
    def send_user_name(self,username):
        # self.loger.info("輸入的用戶名是:"+username)
        self.register_p.get_username_element().send_keys(username)

    #輸入密碼
    def send_user_password(self,password):
        # self.loger.info("輸入的密碼是:"+password)
        self.register_p.get_password_element().send_keys(password)
    
    #輸入驗證碼
    def send_user_code(self,file_name):
        get_code_text = GetCode(self.driver)
        code = get_code_text.code_online(file_name)
        self.register_p.get_code_element().send_keys(code)
    
    #獲取文字信息
    def get_user_text(self,info,user_info):
        try:# 容錯處理
            if info == 'user_email_error':
                text = self.register_p.get_email_error_element().text  # 獲取郵箱錯誤信息
            elif info == 'user_name_error':
                text = self.register_p.get_name_error_element().text  # 獲取用戶名錯誤信息
            elif info == 'password_error':
                text = self.register_p.get_password_error_element().text  # 獲取用戶密碼錯誤信息
            else:
                text = self.register_p.get_code_error_element().text  # 獲取驗證碼錯誤信息
        except:
            text = None     
        return text
    #點擊注冊按鈕
    def click_register_button(self):
        self.register_p.get_button_element().click()
    
    #獲取注冊按鈕文字
    def get_register_btn_text(self):
        """如獲取不到信息,表明頁面已成功跳轉"""
        return self.register_p.get_button_element().text

 

 

6)business包新建register_business.py文件:測試注冊頁面form表單功能邏輯

from handle.register_handle import RegisterHandle
class RegisterBusiness:
    """測試注冊頁面form表單功能情況"""
    def __init__(self,driver):
        self.register_h = RegisterHandle(driver)

    def user_base(self,email,name,password,file_name):
        self.register_h.send_user_email(email)
        self.register_h.send_user_name(name)
        self.register_h.send_user_password(password)
        self.register_h.send_user_code(file_name)
        self.register_h.click_register_button()
    
    def register_succes(self):
        if self.register_h.get_register_btn_text() == None:
            # 注冊成功
            return True
        else:
            return False

    # 郵箱錯誤
    def login_email_error(self,email,name,password,file_name):
        self.user_base(email,name,password,file_name)   
        if self.register_h.get_user_text('user_email_error',"請輸入有效的電子郵件地址") == None:
            #print("無錯誤,郵箱檢驗不成功")
            return True
        else:
            return False
        
   
    def login_name_error(self,email,name,password,file_name):
        self.user_base(email,name,password,file_name)
        if self.register_h.get_user_text('user_name_error',"字符長度必須大於等於4,一個中文字算2個字符") == None:
            #print("用戶名檢驗不成功")
            return True
        else:
            return False
    
    # 密碼錯誤
    def login_password_error(self,email,name,password,file_name):
        self.user_base(email,name,password,file_name)
        if self.register_h.get_user_text('password_error',"最少需要輸入 5 個字符") == None:
            #print("密碼檢驗不成功")
            return True
        else:
            return False

    # 驗證碼錯誤
    def login_code_error(self,email,name,password,file_name):
        self.user_base(email,name,password,file_name)
        if self.register_h.get_user_text('code_text_error',"驗證碼錯誤") == None:
            #print("驗證碼檢驗不成功")
            return True
        else:
            return False

 

7)case包新建first_case.py:測試注冊頁面form表單功能

from selenium import webdriver
from setting import setting
from business.register_business import RegisterBusiness



class FirstCase(object):
    def __init__(self, file_name, url):
        self.driver = webdriver.Chrome()
        self.driver.get(url)
        self.driver.maximize_window()
        self.file_name = file_name
        self.login = RegisterBusiness(self.driver)

    def test_login_success(self):
        login_success = self.login.register_succes()
        if login_success:
            print("注冊成功,case調試失敗!")

    def test_login_email_error(self):
        email_error = self.login.login_email_error('11111@qq.com','aaaa','111111',self.file_name)
        if email_error == True:
            print("Error: email沒有錯誤提示,此條case執行失敗!")

    def test_login_username_error(self):
        self.driver.refresh()
        username_error = self.login.login_name_error('2222222@qq.com','bbbbb','111111',self.file_name)
        if username_error == True:
            print("Error: username沒有錯誤提示,此條case執行失敗!")

    def test_login_password_error(self):
        self.driver.refresh()
        password_error = self.login.login_password_error('333333@qq.com','ccccc','111111',self.file_name)
        if password_error == True:
            print("Error: password沒有錯誤提示,此條case執行失敗!")

    def test_login_code_error(self):
        self.driver.refresh()
        code_error = self.login.login_code_error('44444444@qq.com','dddddddd','111111',self.file_name)
        if code_error == True:
            print("Error: password沒有錯誤提示,此條case執行失敗!")

    def main_run(self):

        self.test_login_email_error()
        self.test_login_username_error()
        self.test_login_password_error()
        self.test_login_code_error()
        self.driver.close()



if __name__ == '__main__':
    file_name = setting.code_path
    url = 'http://www.5itest.cn/register'
    first_case = FirstCase(file_name,url)
    first_case.main_run()

 

代碼遞進方向:

  read.ini.py → find_element.py → register_page.py → register_handle.py → register_business.py → first_case.py


 

附例:

get_code_value.py:獲取注冊頁面圖片,截取驗證碼圖片部分區域,識別驗證碼圖片,獲取驗證碼內容

from PIL import Image
from setting.ShowapiRequest import ShowapiRequest
import time


class GetCode:
    """獲取驗證碼圖片,解析驗證碼圖片並返回驗證碼值"""
    def __init__(self, driver):
        self.driver = driver

    def get_code_image(self, file_name):
        self.driver.save_screenshot(file_name)
        code_element = self.driver.find_element_by_id("getcode_num")
        left = code_element.location['x']
        top = code_element.location['y']
        right = code_element.size['width'] + left
        height = code_element.size['height'] + top
        im = Image.open(file_name)
        img = im.crop((left, top, right, height))
        img.save(file_name)
        time.sleep(1)

    # 解析圖片獲取驗證碼
    def code_online(self, file_name):
        self.get_code_image(file_name)
        r = ShowapiRequest("http://route.showapi.com/184-4", "62626", "d61950be50dc4dbd9969f741b8e730f5")
        r.addBodyPara("typeId", "35")
        r.addBodyPara("convert_to_jpg", "0")
        r.addFilePara("image", file_name)  # 文件上傳時設置
        res = r.post()
        # print("test:",res.text)
        time.sleep(1)
        text = res.json()['showapi_res_body']
        # print(text)
        try:
            code = text['Result']
            return code
        except Exception as e:
            print('code_error:',e)
            return None
get_code_value.py

 

二、Unittest介紹及項目實戰中的運用

1、Unittest簡單使用:

import unittest
class FirstCase01(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        print("所有case執行之前的前置")

    @classmethod
    def tearDownClass(cls):
        print("所有case執行之后的后置")

    def setUp(self):
        print("這個是case的前置條件")
    
    def tearDown(self):
        print("這個是case的后置調鍵")

    @unittest.skip("不執行第一條")  # 跳過此條case 不執行
    def testfirst01(self):
        print("這個第一條case")

    def testfirst02(self):
        print("這是第二條case")
    
    def testfirst03(self):
        print("這是第3條case")
    

if __name__ == '__main__':
    #unittest.main()
    suite = unittest.TestSuite()  # suite容器
    suite.addTest(FirstCase01('testfirst02'))
    suite.addTest(FirstCase01('testfirst01'))
    suite.addTest(FirstCase01('testfirst03'))
    unittest.TextTestRunner().run(suite)
# suite :容器 ,結果是集合類型
suite = unittest.defaultTestLoader.discover(case_path,'unittest_*.py')  # 批量選擇性運行case
#三個參數:第一個傳入路徑;第二個匹配文件名,成功則將該文件內指定的case存入suite容器中;第三個參數默認為None

unittest.TextTestRunner().run(suite)    # 執行上述匹配成功的case



assertFalse(code_error, "msg")   # 用於判斷結果

 

2、在項目中用HTMLTestRunner輸出漂亮的HTML報告 

copy下述HTMLTestRunner代碼,在項目中新建 HTMLTestRunner.py文件,將代碼copy到里面即可使用

"""
A TestRunner for use with the Python unit testing framework. It
generates a HTML report to show the result at a glance.

The simplest way to use this is to invoke its main method. E.g.

    import unittest
    import HTMLTestRunner

    ... define your tests ...

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


For more customization options, instantiates a HTMLTestRunner object.
HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.

    # output to a file
    fp = file('my_report.html', 'wb')
    runner = HTMLTestRunner.HTMLTestRunner(
                stream=fp,
                title='My unit test',
                description='This demonstrates the report output by HTMLTestRunner.'
                )

    # Use an external stylesheet.
    # See the Template_mixin class for more customizable options
    runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'

    # run the test
    runner.run(my_test_suite)


------------------------------------------------------------------------
Copyright (c) 2004-2007, Wai Yip Tung
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.
* Neither the name Wai Yip Tung nor the names of its contributors may be
  used to endorse or promote products derived from this software without
  specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

# URL: http://tungwaiyip.info/software/HTMLTestRunner.html

__author__ = "Wai Yip Tung"
__version__ = "0.8.2"


"""
Change History
 
Version 0.8.2
* Show output inline instead of popup window (Viorel Lupu).
 
Version in 0.8.1
* Validated XHTML (Wolfgang Borgert).
* Added description of test classes and test cases.
 
Version in 0.8.0
* Define Template_mixin class for customization.
* Workaround a IE 6 bug that it does not treat <script> block as CDATA.
 
Version in 0.7.1
* Back port to Python 2.3 (Frank Horowitz).
* Fix missing scroll bars in detail log (Podi).
"""

# TODO: color stderr
# TODO: simplify javascript using ,ore than 1 class in the class attribute?

import datetime
import io
import sys
import time
import unittest
from xml.sax import saxutils


# ------------------------------------------------------------------------
# The redirectors below are used to capture output during testing. Output
# sent to sys.stdout and sys.stderr are automatically captured. However
# in some cases sys.stdout is already cached before HTMLTestRunner is
# invoked (e.g. calling logging.basicConfig). In order to capture those
# output, use the redirectors for the cached stream.
#
# e.g.
#   >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
#   >>>

class OutputRedirector(object):
    """ Wrapper to redirect stdout or stderr """
    def __init__(self, fp):
        self.fp = fp

    def write(self, s):
        self.fp.write(s)

    def writelines(self, lines):
        self.fp.writelines(lines)

    def flush(self):
        self.fp.flush()

stdout_redirector = OutputRedirector(sys.stdout)
stderr_redirector = OutputRedirector(sys.stderr)



# ----------------------------------------------------------------------
# Template

class Template_mixin(object):
    """
    Define a HTML template for report customerization and generation.

    Overall structure of an HTML report

    HTML
    +------------------------+
    |<html>                  |
    |  <head>                |
    |                        |
    |   STYLESHEET           |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |  </head>               |
    |                        |
    |  <body>                |
    |                        |
    |   HEADING              |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |   REPORT               |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |   ENDING               |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |  </body>               |
    |</html>                 |
    +------------------------+
    """

    STATUS = {
    0: 'pass',
    1: 'fail',
    2: 'error',
    }

    DEFAULT_TITLE = 'Unit Test Report'
    DEFAULT_DESCRIPTION = ''

    # ------------------------------------------------------------------------
    # HTML Template

    HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>%(title)s</title>
    <meta name="generator" content="%(generator)s"/>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    %(stylesheet)s
</head>
<body>
<script language="javascript" type="text/javascript"><!--
output_list = Array();
 
/* level - 0:Summary; 1:Failed; 2:All */
function showCase(level) {
    trs = document.getElementsByTagName("tr");
    for (var i = 0; i < trs.length; i++) {
        tr = trs[i];
        id = tr.id;
        if (id.substr(0,2) == 'ft') {
            if (level < 1) {
                tr.className = 'hiddenRow';
            }
            else {
                tr.className = '';
            }
        }
        if (id.substr(0,2) == 'pt') {
            if (level > 1) {
                tr.className = '';
            }
            else {
                tr.className = 'hiddenRow';
            }
        }
    }
}
 
 
function showClassDetail(cid, count) {
    var id_list = Array(count);
    var toHide = 1;
    for (var i = 0; i < count; i++) {
        tid0 = 't' + cid.substr(1) + '.' + (i+1);
        tid = 'f' + tid0;
        tr = document.getElementById(tid);
        if (!tr) {
            tid = 'p' + tid0;
            tr = document.getElementById(tid);
        }
        id_list[i] = tid;
        if (tr.className) {
            toHide = 0;
        }
    }
    for (var i = 0; i < count; i++) {
        tid = id_list[i];
        if (toHide) {
            document.getElementById('div_'+tid).style.display = 'none'
            document.getElementById(tid).className = 'hiddenRow';
        }
        else {
            document.getElementById(tid).className = '';
        }
    }
}
 
 
function showTestDetail(div_id){
    var details_div = document.getElementById(div_id)
    var displayState = details_div.style.display
    // alert(displayState)
    if (displayState != 'block' ) {
        displayState = 'block'
        details_div.style.display = 'block'
    }
    else {
        details_div.style.display = 'none'
    }
}
 
 
function html_escape(s) {
    s = s.replace(/&/g,'&');
    s = s.replace(/</g,'<');
    s = s.replace(/>/g,'>');
    return s;
}
 
/* obsoleted by detail in <div>
function showOutput(id, name) {
    var w = window.open("", //url
                    name,
                    "resizable,scrollbars,status,width=800,height=450");
    d = w.document;
    d.write("<pre>");
    d.write(html_escape(output_list[id]));
    d.write("\n");
    d.write("<a href='javascript:window.close()'>close</a>\n");
    d.write("</pre>\n");
    d.close();
}
*/
--></script>
 
%(heading)s
%(report)s
%(ending)s
 
</body>
</html>
"""
    # variables: (title, generator, stylesheet, heading, report, ending)


    # ------------------------------------------------------------------------
    # Stylesheet
    #
    # alternatively use a <link> for external style sheet, e.g.
    #   <link rel="stylesheet" href="$url" type="text/css">

    STYLESHEET_TMPL = """
<style type="text/css" media="screen">
body        { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; }
table       { font-size: 100%; }
pre         { }
 
/* -- heading ---------------------------------------------------------------------- */
h1 {
    font-size: 16pt;
    color: gray;
}
.heading {
    margin-top: 0ex;
    margin-bottom: 1ex;
}
 
.heading .attribute {
    margin-top: 1ex;
    margin-bottom: 0;
}
 
.heading .description {
    margin-top: 4ex;
    margin-bottom: 6ex;
}
 
/* -- css div popup ------------------------------------------------------------------------ */
a.popup_link {
}
 
a.popup_link:hover {
    color: red;
}
 
.popup_window {
    display: none;
    position: relative;
    left: 0px;
    top: 0px;
    /*border: solid #627173 1px; */
    padding: 10px;
    background-color: #E6E6D6;
    font-family: "Lucida Console", "Courier New", Courier, monospace;
    text-align: left;
    font-size: 8pt;
    width: 500px;
}
 
}
/* -- report ------------------------------------------------------------------------ */
#show_detail_line {
    margin-top: 3ex;
    margin-bottom: 1ex;
}
#result_table {
    width: 80%;
    border-collapse: collapse;
    border: 1px solid #777;
}
#header_row {
    font-weight: bold;
    color: white;
    background-color: #777;
}
#result_table td {
    border: 1px solid #777;
    padding: 2px;
}
#total_row  { font-weight: bold; }
.passClass  { background-color: #6c6; }
.failClass  { background-color: #c60; }
.errorClass { background-color: #c00; }
.passCase   { color: #6c6; }
.failCase   { color: #c60; font-weight: bold; }
.errorCase  { color: #c00; font-weight: bold; }
.hiddenRow  { display: none; }
.testcase   { margin-left: 2em; }
 
 
/* -- ending ---------------------------------------------------------------------- */
#ending {
}
 
</style>
"""



    # ------------------------------------------------------------------------
    # Heading
    #

    HEADING_TMPL = """<div class='heading'>
<h1>%(title)s</h1>
%(parameters)s
<p class='description'>%(description)s</p>
</div>
 
""" # variables: (title, parameters, description)

    HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
""" # variables: (name, value)



    # ------------------------------------------------------------------------
    # Report
    #

    REPORT_TMPL = """
<p id='show_detail_line'>Show
<a href='javascript:showCase(0)'>Summary</a>
<a href='javascript:showCase(1)'>Failed</a>
<a href='javascript:showCase(2)'>All</a>
</p>
<table id='result_table'>
<colgroup>
<col align='left' />
<col align='right' />
<col align='right' />
<col align='right' />
<col align='right' />
<col align='right' />
</colgroup>
<tr id='header_row'>
    <td>Test Group/Test case</td>
    <td>Count</td>
    <td>Pass</td>
    <td>Fail</td>
    <td>Error</td>
    <td>View</td>
</tr>
%(test_list)s
<tr id='total_row'>
    <td>Total</td>
    <td>%(count)s</td>
    <td>%(Pass)s</td>
    <td>%(fail)s</td>
    <td>%(error)s</td>
    <td> </td>
</tr>
</table>
""" # variables: (test_list, count, Pass, fail, error)

    REPORT_CLASS_TMPL = r"""
<tr class='%(style)s'>
    <td>%(desc)s</td>
    <td>%(count)s</td>
    <td>%(Pass)s</td>
    <td>%(fail)s</td>
    <td>%(error)s</td>
    <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td>
</tr>
""" # variables: (style, desc, count, Pass, fail, error, cid)


    REPORT_TEST_WITH_OUTPUT_TMPL = r"""
<tr id='%(tid)s' class='%(Class)s'>
    <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    <td colspan='5' align='center'>
 
    <!--css div popup start-->
    <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
        %(status)s</a>
 
    <div id='div_%(tid)s' class="popup_window">
        <div style='text-align: right; color:red;cursor:pointer'>
        <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
           [x]</a>
        </div>
        <pre>
        %(script)s
        </pre>
    </div>
    <!--css div popup end-->
 
    </td>
</tr>
""" # variables: (tid, Class, style, desc, status)


    REPORT_TEST_NO_OUTPUT_TMPL = r"""
<tr id='%(tid)s' class='%(Class)s'>
    <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    <td colspan='5' align='center'>%(status)s</td>
</tr>
""" # variables: (tid, Class, style, desc, status)


    REPORT_TEST_OUTPUT_TMPL = r"""
%(id)s: %(output)s
""" # variables: (id, output)



    # ------------------------------------------------------------------------
    # ENDING
    #

    ENDING_TMPL = """<div id='ending'> </div>"""

# -------------------- The end of the Template class -------------------


TestResult = unittest.TestResult

class _TestResult(TestResult):
    # note: _TestResult is a pure representation of results.
    # It lacks the output and reporting ability compares to unittest._TextTestResult.

    def __init__(self, verbosity=1):
        TestResult.__init__(self)
        self.stdout0 = None
        self.stderr0 = None
        self.success_count = 0
        self.failure_count = 0
        self.error_count = 0
        self.verbosity = verbosity

        # result is a list of result in 4 tuple
        # (
        #   result code (0: success; 1: fail; 2: error),
        #   TestCase object,
        #   Test output (byte string),
        #   stack trace,
        # )
        self.result = []


    def startTest(self, test):
        TestResult.startTest(self, test)
        # just one buffer for both stdout and stderr
        self.outputBuffer = io.StringIO()
        stdout_redirector.fp = self.outputBuffer
        stderr_redirector.fp = self.outputBuffer
        self.stdout0 = sys.stdout
        self.stderr0 = sys.stderr
        sys.stdout = stdout_redirector
        sys.stderr = stderr_redirector


    def complete_output(self):
        """
        Disconnect output redirection and return buffer.
        Safe to call multiple times.
        """
        if self.stdout0:
            sys.stdout = self.stdout0
            sys.stderr = self.stderr0
            self.stdout0 = None
            self.stderr0 = None
        return self.outputBuffer.getvalue()


    def stopTest(self, test):
        # Usually one of addSuccess, addError or addFailure would have been called.
        # But there are some path in unittest that would bypass this.
        # We must disconnect stdout in stopTest(), which is guaranteed to be called.
        self.complete_output()


    def addSuccess(self, test):
        self.success_count += 1
        TestResult.addSuccess(self, test)
        output = self.complete_output()
        self.result.append((0, test, output, ''))
        if self.verbosity > 1:
            sys.stderr.write('ok ')
            sys.stderr.write(str(test))
            sys.stderr.write('\n')
        else:
            sys.stderr.write('.')

    def addError(self, test, err):
        self.error_count += 1
        TestResult.addError(self, test, err)
        _, _exc_str = self.errors[-1]
        output = self.complete_output()
        self.result.append((2, test, output, _exc_str))
        if self.verbosity > 1:
            sys.stderr.write('E  ')
            sys.stderr.write(str(test))
            sys.stderr.write('\n')
        else:
            sys.stderr.write('E')

    def addFailure(self, test, err):
        self.failure_count += 1
        TestResult.addFailure(self, test, err)
        _, _exc_str = self.failures[-1]
        output = self.complete_output()
        self.result.append((1, test, output, _exc_str))
        if self.verbosity > 1:
            sys.stderr.write('F  ')
            sys.stderr.write(str(test))
            sys.stderr.write('\n')
        else:
            sys.stderr.write('F')


class HTMLTestRunner(Template_mixin):
    """
    """
    def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
        self.stream = stream
        self.verbosity = verbosity
        if title is None:
            self.title = self.DEFAULT_TITLE
        else:
            self.title = title
        if description is None:
            self.description = self.DEFAULT_DESCRIPTION
        else:
            self.description = description

        self.startTime = datetime.datetime.now()


    def run(self, test):
        "Run the given test case or test suite."
        result = _TestResult(self.verbosity)
        test(result)
        self.stopTime = datetime.datetime.now()
        self.generateReport(test, result)
        # print >> sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)
        print(sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime))
        return result


    def sortResult(self, result_list):
        # unittest does not seems to run in any particular order.
        # Here at least we want to group them together by class.
        rmap = {}
        classes = []
        for n,t,o,e in result_list:
            cls = t.__class__
            if not cls in rmap:
                rmap[cls] = []
                classes.append(cls)
            rmap[cls].append((n,t,o,e))
        r = [(cls, rmap[cls]) for cls in classes]
        return r


    def getReportAttributes(self, result):
        """
        Return report attributes as a list of (name, value).
        Override this to add custom attributes.
        """
        startTime = str(self.startTime)[:19]
        duration = str(self.stopTime - self.startTime)
        status = []
        if result.success_count: status.append('Pass %s'    % result.success_count)
        if result.failure_count: status.append('Failure %s' % result.failure_count)
        if result.error_count:   status.append('Error %s'   % result.error_count  )
        if status:
            status = ' '.join(status)
        else:
            status = 'none'
        return [
            ('Start Time', startTime),
            ('Duration', duration),
            ('Status', status),
        ]


    def generateReport(self, test, result):
        report_attrs = self.getReportAttributes(result)
        generator = 'HTMLTestRunner %s' % __version__
        stylesheet = self._generate_stylesheet()
        heading = self._generate_heading(report_attrs)
        report = self._generate_report(result)
        ending = self._generate_ending()
        output = self.HTML_TMPL % dict(
            title = saxutils.escape(self.title),
            generator = generator,
            stylesheet = stylesheet,
            heading = heading,
            report = report,
            ending = ending,
        )
        self.stream.write(output.encode('utf8'))


    def _generate_stylesheet(self):
        return self.STYLESHEET_TMPL


    def _generate_heading(self, report_attrs):
        a_lines = []
        for name, value in report_attrs:
            line = self.HEADING_ATTRIBUTE_TMPL % dict(
                    name = saxutils.escape(name),
                    value = saxutils.escape(value),
                )
            a_lines.append(line)
        heading = self.HEADING_TMPL % dict(
            title = saxutils.escape(self.title),
            parameters = ''.join(a_lines),
            description = saxutils.escape(self.description),
        )
        return heading


    def _generate_report(self, result):
        rows = []
        sortedResult = self.sortResult(result.result)
        for cid, (cls, cls_results) in enumerate(sortedResult):
            # subtotal for a class
            np = nf = ne = 0
            for n,t,o,e in cls_results:
                if n == 0: np += 1
                elif n == 1: nf += 1
                else: ne += 1

            # format class description
            if cls.__module__ == "__main__":
                name = cls.__name__
            else:
                name = "%s.%s" % (cls.__module__, cls.__name__)
            doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""
            desc = doc and '%s: %s' % (name, doc) or name

            row = self.REPORT_CLASS_TMPL % dict(
                style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
                desc = desc,
                count = np+nf+ne,
                Pass = np,
                fail = nf,
                error = ne,
                cid = 'c%s' % (cid+1),
            )
            rows.append(row)

            for tid, (n,t,o,e) in enumerate(cls_results):
                self._generate_report_test(rows, cid, tid, n, t, o, e)

        report = self.REPORT_TMPL % dict(
            test_list = ''.join(rows),
            count = str(result.success_count+result.failure_count+result.error_count),
            Pass = str(result.success_count),
            fail = str(result.failure_count),
            error = str(result.error_count),
        )
        return report


    def _generate_report_test(self, rows, cid, tid, n, t, o, e):
        # e.g. 'pt1.1', 'ft1.1', etc
        has_output = bool(o or e)
        tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
        name = t.id().split('.')[-1]
        doc = t.shortDescription() or ""
        desc = doc and ('%s: %s' % (name, doc)) or name
        tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL

        # o and e should be byte string because they are collected from stdout and stderr?
        if isinstance(o,str):
            # TODO: some problem with 'string_escape': it escape \n and mess up formating
            # uo = unicode(o.encode('string_escape'))
            # uo = o.decode('latin-1')
            uo = e
        else:
            uo = o
        if isinstance(e,str):
            # TODO: some problem with 'string_escape': it escape \n and mess up formating
            # ue = unicode(e.encode('string_escape'))
            # ue = e.decode('latin-1')
            ue = e
        else:
            ue = e

        script = self.REPORT_TEST_OUTPUT_TMPL % dict(
            id = tid,
            output = saxutils.escape(str(uo)+ue),
        )

        row = tmpl % dict(
            tid = tid,
            Class = (n == 0 and 'hiddenRow' or 'none'),
            style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
            desc = desc,
            script = script,
            status = self.STATUS[n],
        )
        rows.append(row)
        if not has_output:
            return

    def _generate_ending(self):
        return self.ENDING_TMPL


##############################################################################
# Facilities for running tests from the command line
##############################################################################

# Note: Reuse unittest.TestProgram to launch test. In the future we may
# build our own launcher to support more specific command line
# parameters like test title, CSS, etc.
class TestProgram(unittest.TestProgram):
    """
    A variation of the unittest.TestProgram. Please refer to the base
    class for command line parameters.
    """
    def runTests(self):
        # Pick HTMLTestRunner as the default test runner.
        # base class's testRunner parameter is not useful because it means
        # we have to instantiate HTMLTestRunner before we know self.verbosity.
        if self.testRunner is None:
            self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
        unittest.TestProgram.runTests(self)

main = TestProgram

##############################################################################
# Executing this module from the command line
##############################################################################

if __name__ == "__main__":
    main(module=None)
HTMLTestRunner

 

參考:

import unittest
from test_mathfunc import TestMathFunc
from HTMLTestRunner import HTMLTestRunner

if __name__ == '__main__':
    suite = unittest.TestSuite()

    suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc))
    with open(r'D:\HTMLReport.html', 'wb') as f:
        runner = HTMLTestRunner(stream=f,
                                title='MathFunc Test Report',
                                description='generated by HTMLTestRunner.',
                                verbosity=2
                                 )
        runner.run(suite)

 

suite = unittest.TestSuite()
    suite.addTest(FirstCase('test_login_success'))
    # suite.addTest(FirstCase('test_login_code_error'))
    suite.addTest(FirstCase('test_login_email_error'))
    suite.addTest(FirstCase('test_login_username_error'))
    with open(setting.report_path, 'wb') as f:
        runner = HTMLTestRunner(stream=f,
                                title="This is first123 report",
                                description="這個是我們第一次測試報告",
                                verbosity=2
                                )
        runner.run(suite)
demo

 * unittest中,執行case時,如果執行失敗,錯誤信息會被保存到_outtime中,我們可通過一些技巧對這些錯誤信息進行操作,比如:case執行完,如果出錯,可以通過tearDown方法 將錯誤信息頁面截圖,保存到指定路徑,方便檢查出錯原因 ↓

    def tearDown(self):
        for method_name, error in self._outcome.errors:  # case如果執行失敗,錯誤會保存到_outcome.errors 中
            if error:  # 將錯誤信息截圖,保存到指定路徑
                case_name = self._testMethodName  # case名,即定義好的方法名
                report_error_name = case_name + '.png'
                report_error_path = os.path.join(setting.base_dir,'report', report_error_name)
                print("report_error:", report_error_name)
                self.driver.save_screenshot(report_error_path)

 


 

使用unittest框架 + HTMLTestRunner 結合,優化 first_case.py 代碼:

from selenium import webdriver
from setting import setting
from business.register_business import RegisterBusiness
import unittest
import os
from util.HTMLTestRunner import HTMLTestRunner


class FirstCase(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        cls.driver = webdriver.Chrome()
        cls.driver.get('http://www.5itest.cn/register')
        cls.driver.maximize_window()
        cls.file_name = setting.code_path
        cls.login = RegisterBusiness(cls.driver)

    def setUp(self):
        self.driver.refresh()
        # self.login = RegisterBusiness(self.driver)


    def tearDown(self):
        for method_name, error in self._outcome.errors:  # case如果執行失敗,錯誤會保存到_outcome.errors 中
            if error:
                case_name = self._testMethodName  # case名,即定義好的方法名
                report_error_name = case_name + '.png'
                report_error_path = os.path.join(setting.base_dir,'report', report_error_name)
                print("report_error:", report_error_name)
                self.driver.save_screenshot(report_error_path)

    @classmethod
    def tearDownClass(cls):
        cls.driver.close()

    def test_login_success(self):
        login_success = self.login.register_succes()
        return self.assertFalse(login_success, "注冊成功,case調試失敗!")

    def test_login_email_error(self):
        email_error = self.login.login_email_error('1586457@qq.com','vvv','221111',self.file_name)
        return self.assertFalse(email_error, "郵箱格式輸入正確,此條case執行失敗!") # 判斷email_error是否為False,如是表示測試通過,如不是表示測試失敗,顯示參數二的信息
        # if email_error == True:
        #     print("Error: email沒有錯誤提示,此條case執行失敗!")

    def test_login_username_error(self):
        # self.driver.refresh()
        username_error = self.login.login_name_error('qq.com','bbbaac','111111',self.file_name)
        return self.assertFalse(username_error, "用戶名格式輸入正確,此條case執行失敗!")

    def test_login_password_error(self):
        # self.driver.refresh()
        password_error = self.login.login_password_error('3333666@qq.com','ccc','1114441',self.file_name)
        return self.assertFalse(password_error, "密碼格式輸入正確,此條case執行失敗!")

    def test_login_code_error(self):
        # self.driver.refresh()
        code_error = self.login.login_code_error('44465745444@qq.com','ddfghdddd','1111',self.file_name)
        return self.assertFalse(code_error, "驗證碼格式輸入正確,此條case執行失敗!")

    # def main_run(self):
    #
    #     self.test_login_email_error()
    #     self.test_login_username_error()
    #     self.test_login_password_error()
    #     self.test_login_code_error()
    #     self.driver.close()



if __name__ == '__main__':
    # file_name = setting.code_path
    # url = 'http://www.5itest.cn/register'
    # first_case = FirstCase(file_name,url)
    # first_case.main_run()

    suite = unittest.TestSuite()
    suite.addTest(FirstCase('test_login_email_error'))
    suite.addTest(FirstCase('test_login_username_error'))
    suite.addTest(FirstCase('test_login_password_error'))
    suite.addTest(FirstCase('test_login_code_error'))
    suite.addTest(FirstCase('test_login_success'))
    with open(setting.report_path, 'wb') as f:
        runner = HTMLTestRunner(stream=f,
                                title="This is first123 report",
                                description="這個是我們第一次測試報告",
                                verbosity=2
                                )
        runner.run(suite)

 


 

二、數據驅動

 使用數據驅動,可以去除冗余。如同一方法傳入不同參數這種,可以使用數據驅動程序,根據需求傳入不同參數,驅動同一程序執行。

1、ddt安裝:pip install ddt

ddt簡單使用:

import ddt
import unittest
@ddt.ddt  # DataTest類加ddt裝飾器
class DataTest(unittest.TestCase):
    def setUp(self):
        print("這個是setup")
    def tearDown(self):
        print("這個是teardown")

    @ddt.data(  # ddt數據
        [1,2,3,4],
        [3,4],
        [5,6]
    )
    @ddt.unpack  #傳遞的是復雜的數據結構時使用。比如使用元組或者列表,不是復雜的數據不需要用到unpack
    def test_add(self,a,b):
        print(a+b)

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

 

使用方法:

# 使用方法
dd.ddt:
# 裝飾類,也就是繼承自TestCase的類。

ddt.data:
# 裝飾測試方法。參數是一系列的值。

ddt.file_data:
# 裝飾測試方法。參數是文件名。文件可以是json 或者 yaml類型。

# 注意,如果文件以”.yml”或者”.yaml”結尾,ddt會作為yaml類型處理,其他所有文件都會作為json文件處理。

# 如果文件中是列表,每個列表的值會作為測試用例參數,同時作為測試用例方法名后綴顯示。

# 如果文件中是字典,字典的key會作為測試用例方法的后綴顯示,字典的值會作為測試用例參數。

ddt.unpack:
# 傳遞的是復雜的數據結構時使用。比如使用元組或者列表,添加unpack之后,ddt會自動把元組或者列表對應到多個參數上。字典也可以這樣處理

 

使用json文件:

新建文件 test_data_list.json:

[
    "Hello",
    "Goodbye"
]

新建文件  test_data_dict.json:

{
    "unsorted_list": [ 10, 12, 15 ],
    "sorted_list": [ 15, 12, 50 ]
}

數據驅動測試腳本ddt_test.py:

import unittest
from ddt import ddt, file_data
from ddt_demo.mycode import has_three_elements,is_a_greeting

@ddt
class FooTestCase(unittest.TestCase):

    @file_data('test_data_dict.json')
    def test_file_data_json_dict(self, value):
        self.assertTrue(has_three_elements(value))

    @file_data('test_data_list.json')
    def test_file_data_json_list(self, value):
        self.assertTrue(is_a_greeting(value))
        
if __name__=='__main__':
    unittest.main(verbosity=2)

 2、以文件的形式實現數據驅動( excel表 )

1)xlrd安裝:

 pip install xlrd

2)通過xlrd 獲取excel表中數據行數,將每行數據以列表形式添加到一個大列表中

import xlrd
from setting.setting import excel_path

class Excel_Opertion(object):
    """excel表數據相關操作"""

    def __init__(self,ex_path=None,index=None):
        if ex_path == None:
            self.excel_path = excel_path  # 默認excel文件路徑
        else:
            self.excel_path = ex_path
        if index == None:
            index = 0
        self.data = xlrd.open_workbook(self.excel_path)
        self.table = self.data.sheets()[index] # sheets第一頁數據

    # 獲取excel數據,按照每行一個list,添加到一個大的list里面
    def get_data(self):
        result = []
        rows = self.get_lines()
        if rows !=None:
            for i in range(1,rows):
                row = self.table.row_values(i)
                # print(row)   # ['test001@qq.com', 'Mushishi001', '111111', 'code', 'user_email_error', '請輸入有效的電子郵件地址']
                result.append(row)   # [['test001@qq.com', 'Mushishi001', '111111', 'code', 'user_email_error', '請輸入有效的電子郵件地址'], ['test002.com', 'Mushishi002', '111112', 'code', 'user_email_error', '請輸入有效的電子郵件地址']]
            return result
        return None

    # 獲取excel行數
    def get_lines(self):
        rows = self.table.nrows  # 獲取行數
        if rows > 1:
            return rows
        return None

 

3)數據驅動case

 從excel表獲取數據,通過ddt模塊實現數據驅動模式,數據 → 驅動 → 程序,循環調用 test_register_case() 方法,直至excel表數據執行完

mport ddt
import unittest
import os
from selenium import webdriver
from setting import setting
from business.register_business import RegisterBusiness
from util.excel_operation import Excel_Opertion
from util.HTMLTestRunner import HTMLTestRunner

ex_opr = Excel_Opertion()  # 實例化
ex_data = ex_opr.get_data()  # 獲取excel表中每行數據

@ddt.ddt
class FirstDdtCase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.driver = webdriver.Chrome()
        cls.driver.get('http://www.5itest.cn/register')
        cls.driver.maximize_window()
        cls.file_name = setting.code_path
        cls.login = RegisterBusiness(cls.driver)

    def setUp(self):
        self.driver.refresh()
        # self.login = RegisterBusiness(self.driver)

    def tearDown(self):
        for method_name, error in self._outcome.errors:  # case如果執行失敗,錯誤會保存到_outcome.errors 中
            if error:
                # case_name = self._testMethodName  # case名,即定義好的方法名
                report_error_name = self.assertCode + '.png'
                report_error_path = os.path.join(setting.base_dir, 'report', report_error_name)
                self.driver.save_screenshot(report_error_path)

    @classmethod
    def tearDownClass(cls):
        cls.driver.close()

    @ddt.data(*ex_data)
    def test_register_case(self,ex_data):   # ex_data:[[],[],..] 列表套列表
        """數據驅動模式,會按ex_data列表數據,一行一行循環執行,直至列表數據執行完畢"""
        email, username, password, self.assertCode, assertText = ex_data  # 將ex_data每個子列表的數據按順序賦值。驗證碼需要提供路徑,單獨給
        # 郵箱、用戶名、密碼、驗證碼、錯誤信息定位元素、錯誤提示信息
        register_error = self.login.register_function(email, username, password, self.file_name,self.assertCode, assertText)
        self.assertFalse(register_error,"測試失敗:{}".format(self.assertCode))

 

test_register_case方法:
# 數據驅動,只執行此條代碼
    # 郵箱、用戶名、密碼、驗證碼、錯誤信息定位元素、錯誤提示信息
    def register_function(self,email,username,password,file_name,assertCode,assertText):
        self.user_base(email,username,password,file_name)
        if self.register_h.get_user_text(assertCode,assertText) == None:
            return True
        else:
            return False
test_register_case

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM