基於python多線程自制快速冒煙測試工具


一、前言

在項目測試的過程中,發現經常會有下面這些情況:

  • 新版本升級后,有時會因為代碼或部署時配置錯誤,某個菜單報類似404或500的錯誤;
  • 為避免以上問題,實施部署人員升級完后,需要手動冒煙一遍所有菜單頁面;
  • 發布當天還有修改提測,來不及再全部手動回歸一遍;
  • 項目周期不長,沒有時間來做業務流程的自動化。

如果所有的菜單都通過手動來冒煙一遍的話,我們假定一個菜單頁面要5秒,那么如果一個項目有600個菜單頁面,那么就需要3000秒,整整一個小時啊,而且是每次升級都要來這么一次。於是我就想,雖然沒時間將所有功能都做成自動化,那能不能實現對所有的菜單頁面都自動去訪問一遍,然后根據訪問結果判斷是不是都是正常的呢?

 

二、實現步驟分析

想法有了,那就得分析該怎么實現了。最大的問題就在於所有菜單對應的url要怎么獲取了,通過各種嘗試之后,發現所測項目由於框架原因,無法直接通過爬蟲來獲取所有的url。

但是在數據庫里,對應前后台都有兩張表,如eclp_A存放有域名,eclp_B則存有各個菜單頁面的具體路徑,那么只需要把兩張表的數據取出來拼接在一起就成了一個完整的url了。

 

具體步驟:

(1)在數據庫里將客戶端和后台管理端(客戶端是uc,后台管理端是eclp)域名和路徑取出來進行拼接放到一個新表,並加入對應斷言關鍵詞;

(2)不同項目的數據庫配置不一樣,所以把數據庫配置放在配置文件,再通過程序獲取;

(3)獲取到數據庫的配置后,連接數據庫,從表里獲取到所有的url;

(4)取到所有的url之后遍歷去request,但是這個需要先登錄獲取到cookie后做個cookie綁定,那么登錄需要用到的用戶密碼也需要放到配置文件;

(5)遍歷訪問所有url,並記錄其訪問結果,成功與失敗個數;

(6)在程序執行訪問頁面時,把成功與失敗的日志都記錄下來,方便后續查找問題;

(7)因為url可能比較多,訪問所有頁面會很慢,所以實現多進程或多線程的方式;

(8)將所有頁面訪問結果利用HTML報告輸出;

 

三、前期准備

1. 在數據庫(此處是oracle)拼接URL

1.1 創建存放url的表

查看后台存放主域名和路徑的表

select t.*,t.rowid from eclp_A t order by id desc;

select t.*,t.rowid from eclp_B t order by id desc;

創建存放完整url的表

drop table eclp_uc_url;

CREATE TABLE eclp_uc_url (

ID NUMBER(20) DEFAULT 0  NOT NULL ,

SUB_SYSTEM_ID  NUMBER(20) DEFAULT 0  NOT NULL ,

SUB_SYSTEM_CODE VARCHAR2(100 BYTE) DEFAULT ''  NULL ,

NAME  VARCHAR2(255 BYTE) DEFAULT ''  NULL , 

DOMAIN VARCHAR2(100 BYTE) DEFAULT ''  NULL ,

PATH VARCHAR2(100 BYTE) DEFAULT ''  NULL ,

URL  VARCHAR2(100 BYTE) DEFAULT ''  NULL ,

ASSERT_WORD  VARCHAR2(255 BYTE) DEFAULT ''  NULL

)

創建主鍵

ALTER TABLE eclp_uc_url ADD PRIMARY KEY (ID);

創建id自增序列

drop SEQUENCE SEQ_ECLP_UC_URL;

CREATE SEQUENCE SEQ_ECLP_UC_URL

INCREMENT BY 1

START WITH 1

MAXVALUE 9999999999999

CYCLE

CACHE 20;

 

1.2 從兩個表分別插入域名和路徑到存放url的新表

eclp的路徑path插入表

INSERT INTO eclp_uc_url(id,SUB_SYSTEM_ID,NAME,PATH) SELECT SEQ_ECLP_UC_URL.NEXTVAL,SUB_SYSTEM_ID,NAME,URL from eclp_A;

eclp的域名domian插入表

update eclp_uc_url set DOMAIN = (select DOMAIN from eclp_B where eclp_uc_url.SUB_SYSTEM_ID = eclp_B.id)
where exists (select 1 from eclp_B where eclp_uc_url.SUB_SYSTEM_ID = eclp_sub_system.id);

將domain和path拼接成url

MERGE INTO eclp_uc_url A
USING ( select t.id, t.domain || '/' || t.path as urls from eclp_uc_url t) B ON (A.ID = B.ID)
WHEN MATCHED THEN UPDATE SET A.URL = B.URLS;

 

2. 數據庫配置與前后台用戶配置文件

創建一個DbUser.ini文件:

存有數據庫連接配置、前后台用戶信息、瀏覽器選擇(chrome、firefox、ie)

[db]
oracle_ip=127.0.0.1
oracle_account=test_account
oracle_password=test_password

[eclp]
center_url=http://127.0.0.1:8020/system/login.htm
center_account=test01
center_password=123456a
 
[uc]
uc_url=http://127.0.0.1:9001/login.htm
uc_account=test02
uc_password=123456a

[driver]
browser=chrome

 

3. 前后台登錄元素定位方式配置文件

包含用戶名、密碼、登錄按鈕的定位方位,UiObjectMap.ini

[eclp]
LoginAccount=id>account
LoginPassword=id>password
LoginButton=xpath>//input[@class='btn2']

[uc]
LoginAccount=id>account
LoginPassword=id>password
LoginButton=id>button

 

四、代碼實現-封裝

1. 創建包和文件夾目錄

Config:存放配置文件

Driver:存放不同瀏覽器驅動

TestResults:存放測試結果

TestScripts:存放程序腳本

Util:存放封裝方法

2. 將所需用到目錄及文件路徑單獨整理

ProjVar.py

import os

proj_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
conf_path = os.path.join(proj_path,"config")
dbuser_ini_path = os.path.join(proj_path,"config","DbUser.ini")
objectmap_ini_path = os.path.join(proj_path,"config","UiObjectMap.ini")
driver_path = os.path.join(proj_path,"Driver")
logger_path = os.path.join(proj_path,"config","Logger.conf")
result_path = os.path.join(proj_path,"Testresults")
testscript_path = os.path.join(proj_path,"TestScripts")

3. 從ini配置文件讀取數據方法:

ReadConfig.py

# encoding=utf-8
import configparser
import os
import platform
from Config.ProjVar import *

def read_ini_file(ini_file_path, section_name, option_name):
    #創建一個讀取配置文件的實例
    cf = configparser.ConfigParser()
    #將配置文件內容加載到內存
    cf.read(ini_file_path)
    try:
        #根據section和option獲取配置文件中的數據
        value = cf.get(section_name, option_name)
    except:
        print("the specific seciton or the specific option doesn't exit!")
        return None
    else:
        return value

4.獲取元素對象方法

根據元素定位方式獲取到對應元素對象,后面登錄時會用到

ObjectMap.py

#encoding=utf-8
from selenium.webdriver.support.ui import WebDriverWait
import configparser,os
from selenium import webdriver
from Config.ProjVar import *
from Util.ReadConfig import read_ini_file

class ObjectMap(object):
    def __init__(self):
        # 存放頁面元素定位表達方式及定位表達式的配置文件所在絕對路徑
        self.uiObjMapPath = objectmap_ini_path

    def getElementObject(self, driver, webSiteName, elementName):
        try:
            locators = read_ini_file(self.uiObjMapPath, webSiteName, elementName).split(">")
            # 得到定位方式
            locatorMethod = locators[0]
            # 得到定位表達式
            locatorExpression = locators[1]
            print(locatorMethod, locatorExpression)
            # 通過顯示等待方式獲取頁面元素
            element = WebDriverWait(driver, 10).until(lambda x: \
                                    x.find_element(locatorMethod, locatorExpression))
        except Exception as e:
            raise e
        else:
            # 當頁面元素被找到后,將該頁面元素對象返回給調用者
            return element

 

5. 建立一個綁定cookies的session對象

從元素定位方式配置文件獲取到定位方式,然后用ObjectMap方法獲取到元素對象,用selenium webdriver自動登錄,登錄成功后得到cookies

 GetSessionOfCookie.py

from selenium import webdriver
import requests,time,os
from Config.ProjVar import *
from Util.ReadConfig import read_ini_file
from Util.ObjectMap import *
#from selenium.webdriver.chrome.options import Options

def get_session_of_cookie(domain,url,account,password):
    #從配置文件獲取配置的瀏覽器類型,並對應去登錄獲取cookie
    browser = read_ini_file(dbuser_ini_path, "driver", "browser")
    if browser.lower() == "chrome":
        driverpath = os.path.join(driver_path, "chromedriver.exe")
        driver = webdriver.Chrome(executable_path=driverpath)
    elif browser.lower() == "firefox":
        driverpath = os.path.join(driver_path,"geckodriver.exe")
        driver = webdriver.Firefox(executable_path=driverpath)
    elif browser.lower() == "ie":
        driverpath = os.path.join(driver_path, "IEDriverServer.exe")
        driver = webdriver.Ie(executable_path=driverpath)
    driver.maximize_window()
    time.sleep(1)
    #打開前后台登錄頁面
    driver.get(url)
    driver.implicitly_wait(5)
    # 獲取登錄頁面元素傳值登錄
    objmap = ObjectMap()
    if domain == "eclp":
        objmap.getElementObject(driver, "eclp", "LoginAccount").send_keys(account)
        objmap.getElementObject(driver, "eclp", "LoginPassword").send_keys(password)
        objmap.getElementObject(driver, "eclp", "LoginButton").click()
    elif domain == "uc":
        objmap.getElementObject(driver, "uc", "LoginAccount").send_keys(account)
        objmap.getElementObject(driver, "uc", "LoginPassword").send_keys(password)
        objmap.getElementObject(driver, "uc", "LoginButton").click()
    time.sleep(3)
    #獲取登錄后的cookies
    allcookies = driver.get_cookies()
    print("獲取到登錄后的cookies:%s" % allcookies)
    driver.quit()
    #把上面獲取的的cookies添加到s中
    s = requests.session()
    try:
        # 添加cookies到CookieJar
        c = requests.cookies.RequestsCookieJar()
        for i in allcookies:
            c.set(i["name"], i['value'])
        # 更新session里cookies
        s.cookies.update(c)  
    except Exception as e:
        print(u"添加cookies報錯:%s" %str(e))
    print("查看添加后s的cookies")
    print(s.cookies)
    return s

 

6. 從數據庫表查詢URL

 這里使用的是cx_Oracle連接oracle,注意需要使用與python位數(32或64)對應的數據庫instantclient客戶端:

GetUrlFromOra.py

import cx_Oracle
from Util.ReadConfig import read_ini_file
from Config.ProjVar import *

def get_url_from_oracle(ip,account,password,domain):
    db=cx_Oracle.connect(account+'/'+password+'@'+ip+'/orcl')
    cr = db.cursor()
    sql = ""
    #根據是eclp還是uc來獲取前端還是后端的url
    if domain == "eclp":
        sql = 'select sub_system_id,name,url,assert_word from eclp_uc_url where sub_system_id != 0  order by id desc'
    elif domain == "uc":
        sql = 'select sub_system_id,name,url,assert_word from eclp_uc_url where sub_system_id = 0 order by id desc'
    cr.execute(sql)
    result = cr.fetchall()
    #返回獲取到的所有結果
    return result

 

7. 創建存放測試結果的文件夾方法

先創建一個執行當天日期為名稱的文件夾,若一天執行了多次,在日期文件夾下建一個“第n次”為名稱的文件夾

#encoding = utf-8
import os
import time
from Config.ProjVar import *

#創建日期格式文件夾
def make_date_dir(dir_path):
    if os.path.exists(dir_path):
        #獲取當前時間
        timeTup = time.localtime()
        #轉為xxxx年xx月xx日的格式
        currentDate = str(timeTup.tm_year) + "" + str(timeTup.tm_mon) + "" + str(timeTup.tm_mday) + ""
        #用目標目錄拼接日期得到絕對路徑
        path = os.path.join(dir_path,currentDate)
        if not os.path.exists(path):
            os.mkdir(path)
    else:
        raise Exception("dir_path does not exist!")
    return path

#創建日期文件夾下多次執行的目錄
def make_report_dir():
    #先創建一個日期格式為名稱的文件夾
    date_path = make_date_dir(result_path)
    #判斷當前目錄已有文件夾數,加1得到新文件夾名並創建
    report_path = os.path.join(date_path, "" + str(len(os.listdir(date_path)) + 1) + "次測試")
    os.mkdir(report_path)
    #進入到新創建文件夾並獲取當前的絕對路徑,作為后面存放測試結果的文件夾
    os.chdir(report_path)
    result_report_path = os.getcwd()
    return result_report_path

 

8. Log日志設置

使用python的Logger.conf配置文件,設置我們需要的日志級別和存放目錄

 

9. HTML報告模板

創建一個htmlTemplate方法,里面先用HTML語言設置好自己想要的報告模板,后面將測試結果與模板拼接即可。

 

五、主程序實現

從數據庫獲取url方法封裝:斷言關鍵詞等數據放進隊列中,並返回前台客戶端uc和后台管理端eclp綁定cookie的session對象

def get_urls(domain,q):
    #判斷是前端uc還是后端eclp去獲取對應的登錄數據
    if domain == "eclp":
        login_url = read_ini_file(dbuser_ini_path, "eclp", "center_url")
        account = read_ini_file(dbuser_ini_path, "eclp", "center_account")
        password = read_ini_file(dbuser_ini_path, "eclp", "center_password")
    elif domain == "uc":
        login_url = read_ini_file(dbuser_ini_path, "uc", "uc_url")
        account = read_ini_file(dbuser_ini_path, "uc", "uc_account")
        password = read_ini_file(dbuser_ini_path, "uc", "uc_password")
    #獲取eclp或uc的綁定cookie的session對象
    s = get_session_of_cookie(domain,login_url,account,password)
    #從配置文件獲取數據庫的連接配置
    ora_ip = read_ini_file(dbuser_ini_path, "db", "oracle_ip")
    ora_account = read_ini_file(dbuser_ini_path, "db", "oracle_account")
    ora_password = read_ini_file(dbuser_ini_path, "db", "oracle_password")
    #從數據庫獲取所有的url
    url_datas = get_url_from_oracle(ora_ip,ora_account,ora_password,domain)
    #將獲取到的所有url放入隊列中
    for data in url_datas:
        q.put(data)
    return s

執行獲取測試結果方法封裝:

在隊列不為空時,循環取得url、斷言關鍵詞等數據,然后利用綁定了cookied的session對象訪問得到結果;

獲取資源鎖記錄成功與失敗個數,並將結果放入一個共享的html字符串中,以便最后組裝成完成的HTML報告:

def get_urls_info():
    #設置測試結果為全局變量
    global share_result
    global success_case_num
    global fail_case_num
    flagDict = {0: 'red', 1: '#00AC4E'}
    #隊列不為空時,循環獲取數據
    while not q.empty():
        #每個測試數據的測試結果,success或fail
        test_case_result = ""
        start = time.time()
        start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        #取隊列
        data = q.get()
        #分別取出要用到的菜單名稱、url、斷言關鍵詞等數據
        sub_system_id = data[0]
        url_name = data[1]
        url = data[2]
        assert_word = data[3]
        #存放斷言失敗的菜單的源碼文件路徑
        html_file_path = os.path.join(result_report_path, url_name + ".html")
        #訪問菜單頁面並記錄結果
        try:
            判斷是eclp的還是uc的菜單,用不同的session對象去訪問
            if sub_system_id == 0:
                r = uc_s.get(url)
            else:
                r = eclp_s.get(url)
            #斷言
            assert assert_word in r.text
            flag = 1
            debug("菜單: " + url_name + " 斷言成功:")
            #記錄當次是否訪問成功
            test_case_result = "Success"
            #獲得資源鎖,記錄成功個數
            lock.acquire()
            success_case_num += 1
            lock.release()
        except AssertionError as e:
            flag = 0
            error("菜單: " + url_name+" 關鍵詞斷言失敗,錯誤信息:" + str(traceback.format_exc()))
            test_case_result = "Fail"
            #如果失敗,獲取資源鎖,記錄失敗數
            lock.acquire()
            fail_case_num += 1
            lock.release()
            #將失敗的菜單頁面的源碼保存為一個html文件,存到測試結果目錄
            with open(html_file_path, "w", encoding="utf-8") as fp:
                fp.write(r.text)
        except Exception as e:
            flag = 0
            error("菜單: " + url_name+" 發生未知異常,錯誤信息:" + str(traceback.format_exc()))
            test_case_result = "Fail"
            lock.acquire()
            fail_case_num += 1
            lock.release()
            with open(html_file_path, "w", encoding="utf-8") as fp:
                fp.write(r.text)
        finally:
            end = time.time()
            waste_time = int((end - start) * 1000)
            #獲取資源鎖,將當次訪問的菜單名稱、url斷言關鍵詞、時間等添加到html字符串
            lock.acquire()
            # 每一組數據測試結束后,都將其測試結果信息插入表格行的HTML代碼中
            share_result += '''
                  <tr>
                      <td>%s</td>
                      <td>%s</td>
                      <td>%s</td>
                      <td>%s</td>
                      <td>%s</td>
                      <td style="color:%s">%s</td>
                  </tr>''' % (url_name, url, assert_word, waste_time, start_time, flagDict[flag], test_case_result)
            lock.release()

 

由於這里更多的是文本、多IO的操作,因此我們選擇多線程來實現:

#定義一個繼承threading.Thread線程的類
class MyThread(threading.Thread):
    def __init__(self, func):
        threading.Thread.__init__(self)
        self.func = func
    def run(self):
        self.func()

main程序:

if __name__ == "__main__":
    # 創建用來存放日志和報告的文件夾
    result_report_path = make_report_dir()
    #建一個線程共享資源鎖
    lock = threading.Lock()
    #定義一個隊列
    q = Queue()
    #定義測試結果變量
    share_result = ""
    success_case_num = 0
    fail_case_num = 0
    #獲取前后台的綁定了cookie的session對象
    eclp_s = get_urls("eclp",q)
    uc_s = get_urls("uc",q)

    #創建多個線程,並將get_urls_info任務傳入處理
    threads = []
    for i in range(4):
        thread = MyThread(get_urls_info)
        thread.start()
        threads.append(thread)
    for thread in threads:
        thread.join()
    #將測試結果與模板拼接得到HTML報告
    htmlTemplate(share_result,result_report_path,success_case_num,fail_case_num)

 

六、測試結果

在測試結果目錄Testresults中有不同日期的文件夾:

下層是一天中多次執行的文件夾,要看最新的測試結果進入“第n次”中n最大的文件夾即可:

 

進入后可以查看到訪問失敗菜單的html源碼,若執行后有失敗的菜單,可以打開查看里面源碼是什么樣的;

另外還有當次執行的《冒煙測試報告》,打開可以查看總個數、成功個數、失敗個數以及每個菜單的執行情況,

 失敗的菜單頁面對應測試結果會顯示紅色:

 

在程序腳本目錄下會生成一個日志文件SmokingTestLog.log,每次執行的日志都會增量記錄在這里:

 

 

七、總結

至此快速的冒煙測試就完成了,基本實現了可以從數據庫獲取所有菜單頁面URL,並利用多線程遍歷訪問,並記錄結果生成HTML報告的功能。

可應用場景:

(1)開發人員開發完成,在開發環境執行快速冒煙,檢查是否有異常頁面;

(2)新功能提測后,測試人眼執行快速冒煙,檢查是否有異常頁面再深入測試;

(3)發布UAT之前,執行快速冒煙,回歸是否有頁面異常

(4)實施人員升級后,執行快速冒煙,檢驗是否部署、配置正確,避免線上出現異常

不同的項目可能模式有所不同,如果菜單未放在表里,則需要看是否可以直接爬取或是其他方式實現了。

若文中有不正確的地方,或有更好的實現方法,歡迎指出並提寶貴意見。


免責聲明!

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



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