傳統(tǒng)軟件測試行業(yè)是以手工測試為主,也就是所謂的點點點 ,加上國內(nèi)軟件公司不注重測試,受制于大環(huán)境影響等也就給了大眾一種測試人員雖然身處互聯(lián)網(wǎng)行業(yè),卻是毫無技術(shù)可言的工種。
話鋒一轉(zhuǎn),到了如今,不得不說一聲:大人,時代變了 ,最直觀的表現(xiàn)莫過于招聘要求的提高,越來越要求測試人員擁有七十二變 的能力。而在這其中,自動化測試能力是現(xiàn)在手工測試邁向更高技術(shù)崗位的必經(jīng)之路。
大家好,我是黎潘,我又來了,作為一名行業(yè)新手,我也是興致滿滿,選擇了當下較為火熱,且入門簡單的Python語言作為我邁向自動化測試工程師的重要幫手 。所以以下討論的皆是與python相關的如何實現(xiàn)自動化的總結(jié),當然肯定不止這一門語言可以實現(xiàn),最好與實際項目需求和個人能力相結(jié)合,選擇最適合自己的自動化測試之路。
二、初識自動化測試 廣義上來講,自動化包括一切通過工具(程序)的方式來代替或輔助手工測試的行為都可以看作是自動化。狹義上來講,通過工具記錄或編寫腳本的方式模擬手工測試的過程,通過回放或運行腳本來執(zhí)行測試用例,從而代替人工對系統(tǒng)的功能進行驗證。通俗易懂點就是一切能代替手工來執(zhí)行測試用例,提高效率,不斷回歸的測試方法,在我眼里都能算是自動化測試。
2. 為什么要做自動化測試 2.1 減少手工測試占比自動化測試可以替代大量的手工機械重復性操作,測試工程師可以把更多的時間花在更全面的用例設計新性功能的測試上。
2.2 提升回歸效率自動化測試可以大幅提升回歸測試的效率,測試人員不用花費大量時間去校驗原有功能的正確性,最大的優(yōu)點是非常適合敏捷開發(fā)過程中,也就是加入到CI/CD中。
2.3 持續(xù)測試系統(tǒng)的穩(wěn)定自動化測試可以高效實現(xiàn)某些手工測試無法完成或者代價巨大的測試類型。比如關鍵核心業(yè)務需要24小時持續(xù)運行的穩(wěn)定性測試。
2.4 增加競爭力隨著測試行業(yè)的發(fā)展,測試人們的發(fā)展方向越來越廣,技術(shù)方向越來越多樣化,更多的測試人傾向于往高技術(shù)攀爬。而擁有自動化測試的能力在以后很有可能是我們選擇工作的敲門磚了。雖然不少人都對這種變化感到惶恐不安,但是更多的人選擇站在狂風處,迎接挑戰(zhàn),增加自身的競爭力,擁抱明天。
3. 什么項目適合自動化測試 3.1 需求穩(wěn)定,不頻繁變更測試腳本的穩(wěn)定性決定了自動化測試的維護成本。如果軟件需求變動過于頻繁,測試人員需要根據(jù)變動的需求來更新測試用例以及相關的測試腳本,而腳本的維護本身就是一個代碼開發(fā)的過程,需要修改,調(diào)試,必要的時候還要修改自動化框架,如果花費的成本高于其節(jié)省的成本,那么自動化測試是失敗的。
我們可以優(yōu)先對項目中核心模塊,相對穩(wěn)定的模塊進行自動化,而變動較大的仍是用手工測試。
3.2 研發(fā)和維護周期長由于自動化測試需求的確定,自動化測試框架的設計,測試腳本的編寫與調(diào)試均需要相當長的時間來完成。這樣的過程本身就是一個測試軟件地開發(fā)過程,需要較長的時間來完成。如果項目周期比較短,沒有足夠的時間去支持這樣一個過程,那么自動化測試便毫無意義。
3.3 項目資源足夠自動化測試從需求范圍的確定,到自動化測試框架的設計,以及腳本的編寫與調(diào)試,均需要相當長的時間來完成。這樣的過程本身就是一個測試軟件的開發(fā)過程。因此有足夠的人力,物力非常重要。
三、搭建自己的接口測試框架 3.1 構(gòu)建接口測試思維當前互聯(lián)網(wǎng)產(chǎn)品最大的特點就是快 ,上線周期通常是以"天"甚至是以"小時"為單位,而傳統(tǒng)軟件產(chǎn)品的周期多以"月",甚至以"年"為單位。因此,如何在保證產(chǎn)品質(zhì)量下,有效縮短測試回歸時間成了重中之重。
兩個突破口:
引入測試的并發(fā)執(zhí)行,即從以往的串行執(zhí)行測試用例,采用分布式的方法并行執(zhí)行。 從測試策略上找到突破口,從傳統(tǒng)軟件產(chǎn)品的金字塔測試策略往菱形測試策略轉(zhuǎn)變。以接口測試為主,GUI測試為輔,單元測試則根據(jù)公司實際情況進行。 四點建議:
輕量級的GUI測試,只覆蓋最核心 直接影響主營業(yè)務 流程的E2E場景 最上層的GUI測試通常利用探索式測試思維 ,以人工測試的方式 發(fā)現(xiàn)盡可能多的潛在問題 單元測試只對那些相對穩(wěn)定并且核心 的服務和模塊開展全面的單元測試,而應用層或者上層業(yè)務只會做少量的 3.2 搭建自己的接口測試框架 3.2.1 為何要搭建自己的測試框架開發(fā)自己的框架更能結(jié)合自身工作中的痛點,難點來做一個針對性的解決,使其擴展性更高,后期也能接入CI/CD。 利用現(xiàn)有工具來進行接口測試,隨著項目的規(guī)模變大,維護成本將會增大,不利于管控。 工具本身具有一定的局限性,如支持的協(xié)議比較單一。 不用糾結(jié)技術(shù)選型,根據(jù)自身的技術(shù)實力和技術(shù)功底 來選擇,而不要以開發(fā)工程師的技術(shù)棧來選擇。 3.2.2 定義專屬框架目錄結(jié)構(gòu)test_data:存放測試數(shù)據(jù) fixture:類似unittest中的setUp/tearDown的存在,但功能遠比他們強大 3.2.3 構(gòu)建框架流程在框架構(gòu)建過程中,由于篇符有限,本文只涉及其中部分環(huán)節(jié)。
1、在common公共模塊、封裝定義框架專屬的http請求能力
# !/usr/bin/python3 # -*- coding: utf-8 -*- # @Author: pan-li import requestsclass HttpRequests (object) : def __init__ (self, url) : self.url = url self.req = requests.session() # 自定義請求頭,根據(jù)自身所在公司項目需求 self.headers = {'Content-Type' : 'application/json' , 'User-Agent' : 'Node midway-v2x Version/1.28.1' } # 封裝get請求 def get (self, url='' , params='' , data='' , headers=None, cookies=None) : response = self.req.get(url=url, params=params, data=data, headers=headers, cookies=cookies) return response # post請求 def post (self, url='' , params='' , data='' , headers=None, cookies=None) : response = self.req.post(url=url, params=params, data=data, headers=headers, cookies=cookies) return response # put請求 def put (self, url='' , params='' , data='' , headers=None, cookies=None) : response = self.req.put(url=url, params=params, data=data, headers=headers, cookies=cookies) return response # delete請求 def delete (self, url='' , params='' , data='' , headers=None, cookies=None) : response = self.req.delete(url=url, params=params, data=data, headers=headers, cookies=cookies) return response
2、抽離URL生成url_conf.py在config文件中
import enumclass URLConf (enum.Enum) : TEST_URL = 'http://10.12.7.20:8443/v2x-omp/api/'
3、編寫接口測試用例在test_case文件中,第一版測試用例,安裝pytest,pip install -U pytest
import osimport sysimport pytestimport jsonfrom common.http_requests import *from config.url_conf import URLConf project_root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) sys.path.append(project_root)class TestV2x : @classmethod def setup_class (cls) -> None : cls.url = URLConf.TEST_URL.value cls.http = HttpRequests(cls.url) def setup (self) -> None : self.headers = {'Content-Type' : 'application/json' , 'User-Agent' : 'Node midway-v2x Version/1.28.1' } self.http = HttpRequests(self.url) def tearDown (self) : pass @staticmethod def get_token () : headers = {'Content-Type' : 'application/json' , 'User-Agent' : 'Node midway-v2x Version/1.28.1' } response = TestV2x.http.post(url=URLConf.TEST_URL.value, data='{"cmd":"signin","params":{"userName":"smarttest","password":"72be4b7f62832c516b85fb26de59df53"}}' , headers=headers) token = response.json()['detail' ]['token' ] return token def test_001_queryArea (self) : """查詢區(qū)域""" playload = {"cmd" : "queryArea" , "csrfToken" : TestV2x.get_token(), "params" : {"cityId" : "320200" }} response = TestV2x.http.post(self.url, data=json.dumps(playload), headers=self.headers) resultNote = response.json().get('resultNote' ) assert resultNote, 'Success' def test_002_queryYearlyCheckCount (self) : """查詢年檢總數(shù)""" playload = {"cmd" : "queryYearlyCheckCount" , "Token" : TestV2x.get_token(), "params" : {}} response = TestV2x.http.post(self.url, data=json.dumps(playload), headers=self.headers) resultNote = response.json().get('resultNote' ) assert resultNote, 'SUCCESS' def test_003_queryTrafficEvent (self) : """查詢交通事件""" playload = {"cmd" : "queryTrafficEvent" , "Token" : TestV2x.get_token(), "params" : {}} response = TestV2x.http.post(self.url, data=json.dumps(playload), headers=self.headers) resultNote = response.json().get('resultNote' ) assert resultNote, 'Success' def test_004_queryRsuCount (self) : """查詢rsu總數(shù)""" playload = {"cmd" : "queryRsuCount" , "Token" : TestV2x.get_token(), "params" : {}} response = TestV2x.http.post(self.url, data=json.dumps(playload), headers=self.headers) resultNote = response.json().get('resultNote' ) assert resultNote, '查詢路測設備數(shù)量成功!' def test_005_queryDeviceDetail (self) : """查詢設備詳情""" playload = {"cmd" : "queryDeviceDetail" , "params" : {"deviceId" : '0086860703231572' }, "Token" : TestV2x.get_token()} response = TestV2x.http.post(self.url, data=json.dumps(playload), headers=self.headers) resultNote = response.json().get('resultNote' ) assert resultNote, '查詢終端信息成功!' if __name__ == '__main__' : pytest.main()
4、顯然前面的測試用例也是流水賬似的,還有很大的優(yōu)化空間,現(xiàn)在就來一步一步進行。
5、優(yōu)化一 :利用feature特性優(yōu)化前置和后置條件,fixture目錄下的v2x_fixture.py文件
import pytestfrom common.http_requests import HttpRequestsfrom config.url_conf import URLConf@pytest.fixture(scope='function', autouse=True) def http () : url = URLConf.TEST_URL.value http = HttpRequests(url) return http@pytest.fixture(scope='function', autouse=True) def get_token (http) : headers = {'Content-Type' : 'application/json' , 'User-Agent' : 'Node midway-v2x Version/1.28.1' } response = http.post(url=URLConf.TEST_URL.value, data='{"cmd":"signin","params":{"userName":"smarttest","password":"72be4b7f62832c516b85fb26de59df53"}}' , headers=headers) token = response.json()['detail' ]['token' ] return token
上述在引入feature之后,簡化了http請求的調(diào)用,重新定義http()來進行調(diào)用。之前每次接口的調(diào)用都要附帶token參數(shù),現(xiàn)在把獲取token的方法提取出來,單獨封裝,加上feature的裝飾,他會作用與每一個方法,用起來更加方便。此處的token是依賴登陸接口之后返回的值,可根據(jù)自身項目的需求封裝。
6、優(yōu)化二: 為測試用例添加數(shù)據(jù)驅(qū)動模式
# 以第五個測試用例單獨為例 @pytest.mark.parametrize('deviceid', ['0086860703231572', '0086337601270714', '0086822412608154']) def test_005_queryDeviceDetail (self, http, get_token, deviceid) : """查詢設備詳情""" playload = {"cmd" : "queryDeviceDetail" , "params" : {"deviceId" : deviceid}, "Token" : get_token} response = http.post(url=URLConf.TEST_URL.value, data=json.dumps(playload), headers=URLConf.HEADERS.value) resultNote = response.json() assert resultNote.get('resultNote' ), '查詢終端信息成功!' logger.info('查詢終端信息成功!' )"""直接利用pytest.mark.parametrize()裝飾器,第一個參數(shù)為參數(shù)名,后邊數(shù)組為測試數(shù)據(jù),用例當中同樣添加形參deviceid"""
在 pytest 中,數(shù)據(jù)驅(qū)動是經(jīng)由 pytest 自帶的 pytest.mark.parametrize() 來實現(xiàn)的。pytest.mark.parametrize 是 pytest 的內(nèi)置裝飾器,它允許你在 function 或者 class 上定義多組參 數(shù)和 fixture 來實現(xiàn)數(shù)據(jù)驅(qū)動。
**@pytest.mark.parametrize() ** 裝飾器接收兩個參數(shù):
第一個參數(shù)以字符串的形式存在,它代表能被被測試函數(shù)所能接受的參數(shù),如果被測試函數(shù)有多個參數(shù), 則以逗號分 第二個參數(shù)用于保存測試數(shù)據(jù)。如果只有一組數(shù)據(jù),以列表的形式存在,如果有多組數(shù)據(jù),以列表嵌套元 組的形式存在 7、優(yōu)化三: 為測試用例添加標簽,此時用到pytest.ini配置文件,放在項目任意位置都能生效,有以下作用
[pytest] python_files = test_* *_test test* python_classes = Test* test* python_functions = test_* test* markers = smoke: marks tests as smoke test : marks tests as test log : marks tests as log# 使用時只需要在測試用例上使用@pytest.mark.smoke即可 # 執(zhí)行時pytest -m [標記名]
8、優(yōu)化四: 配置pytest.ini文件集成日志收集和實時控制臺打印功能
[pytest] log_cli = 1 log_cli_level = DEBUG log_cli_date_format = %Y-%m-%d-%H-%M-%S log_cli_format = %(asctime)s - %(filename)s - %(name)s - %(module)s - %(funcName)s - %(lineno)d - %(levelname)s - %(message)s log_file = ..\\report\\run.log log_file_level = DEBUG log_file_date_format = %Y-%m-%d-%H-%M-%S log_file_format = %(asctime)s - %(filename)s -%(name)s - %(module)s - %(funcName)s - %(lineno)d - %(levelname)s - %(message)s
關于字段的詳解可以在終端輸入pytest --help 查看
9、優(yōu)化五: 定制測試框架測試報告,屬于第三方應用放在lib目錄中
這里我們使用目前市面上使用人數(shù)較多的一款開源測試報告框架Allure,它支持絕大多數(shù)測試框架
安裝方法:
pip install -U allure-pytest github上下載最新版本放到lib目錄,并配置成系統(tǒng)環(huán)境變量(:https://github.com/allure-framework/allure2/releases) 使用方法:
執(zhí)行pytest命令,并指定allure報告目錄:pytest -v -s test_v2x_api_02.py --alluredir=./allure_reports 在線生成allure報告:allure serve allure_reports 生成本地allure報告:allure generate allure_reports 當然這只是在控制臺直接命令執(zhí)行,還不夠方便,如果我們想在其他環(huán)境運行就又得配置環(huán)境變量,那么我們?nèi)绾伟阉傻轿覀兊目蚣苤心?/p>
在共同方法中生成allure工具類,以便分辨運行環(huán)境是windows還是mac
import osimport sysimport platform path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'lib' ) allure_path = os.path.join(path, 'allure' , 'bin' ) sys.path.append(allure_path)class Report () : @property def allure (self) : if platform.system() == 'Windows' : cmd = os.path.join(allure_path, 'allure.bat' ) else : cmd = os.path.join(allure_path, 'allure' ) return cmd
10、在main模塊中,添加執(zhí)行調(diào)度策略
import osimport threadingimport pytestfrom common.report import Report project_root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) report_dir = os.path.join(project_root, 'report' ) result_dir = os.path.join(report_dir, 'allure_result' ) allure_report = os.path.join(report_dir, 'allure_report' ) report = Report()def run_pytest () : pytest.main(['-v' , '-s' , f'--alluredir={result_dir} ' ])def general_report () : cmd = "{} generate {} -o {} --clean" .format(report.allure, result_dir, allure_report) print(os.popen(cmd).read())if __name__ == '__main__' : run = threading.Thread(target=run_pytest) gen = threading.Thread(target=general_report) run.start() # 多線程先執(zhí)行pytest命令生成測試報告 run.join() gen.start() # 報告生成后調(diào)用allure工具類生成本地報告
11、最后一版測試用例,整合前面的優(yōu)化
import osimport sysimport jsonfrom fixture.v2x_fixture import *from config.url_conf import URLConf project_root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) sys.path.append(project_root)class TestV2x : @pytest.mark.smoke # 標簽的使用 def test_001_queryArea (self, http, get_token) : """查詢區(qū)域""" playload = {"cmd" : "queryArea" , "csrfToken" : get_token, "params" : {"cityId" : "320200" }} response = http.post(url=URLConf.TEST_URL.value, data=json.dumps(playload), headers=URLConf.HEADERS.value) resultNote = response.json() assert resultNote.get('resultNote' ), 'success' logger.info('查詢區(qū)域成功' ) def test_002_queryYearlyCheckCount (self, http, get_token) : """查詢年檢總數(shù)""" playload = {"cmd" : "queryYearlyCheckCount" , "Token" : get_token, "params" : {}} response = http.post(url=URLConf.TEST_URL.value, data=json.dumps(playload), headers=URLConf.HEADERS.value) resultNote = response.json() assert resultNote.get('resultNote' ), 'SUCCESS' logger.info('查詢年檢成功' ) def test_003_queryTrafficEvent (self, http,get_token) : """查詢交通事件""" playload = {"cmd" : "queryTrafficEvent" , "Token" : get_token, "params" : {}} response = http.post(url=URLConf.TEST_URL.value, data=json.dumps(playload), headers=URLConf.HEADERS.value) resultNote = response.json() assert resultNote.get('resultNote' ), 'Success' logger.info('查詢交通事件成功' ) def test_004_queryRsuCount (self, http, get_token) : """查詢rsu總數(shù)""" playload = {"cmd" : "queryRsuCount" , "Token" : get_token, "params" : {}} response = http.post(url=URLConf.TEST_URL.value, data=json.dumps(playload), headers=URLConf.HEADERS.value) resultNote = response.json() assert resultNote.get('resultNote' ), '查詢路測設備數(shù)量成功!' # text = response.text # print(text) logger.info('查詢路側(cè)設備成功' ) # 簡單的數(shù)據(jù)驅(qū)動 @pytest.mark.parametrize('deviceid', ['0086860703231572', '0086337601270714', '0086822412608154']) def test_005_queryDeviceDetail (self, http, get_token, deviceid) : """查詢設備詳情""" playload = {"cmd" : "queryDeviceDetail" , "params" : {"deviceId" : deviceid}, "Token" : get_token} response = http.post(url=URLConf.TEST_URL.value, data=json.dumps(playload), headers=URLConf.HEADERS.value) resultNote = response.json() assert resultNote.get('resultNote' ), '查詢終端信息成功!' logger.info('查詢終端信息成功!' )if __name__ == '__main__' : # 打印更詳細的信息 pytest.main(['-s' , '-v' , ])
四、總結(jié) 關于這次自動化測試學習分享,涉及到的知識,只是冰山一角,參加狂師老師的全棧測開訓練營收獲非常大,還有很多的知識點沒有使用到,動手開發(fā)的測試框架依然還有很多優(yōu)化的空間,后續(xù)我會繼續(xù)加油,將細節(jié)補充到位,同時分享一些高階的用法。
如果你覺得文章還不錯,請大家 點贊 、 轉(zhuǎn)發(fā) 、 關注 ,因為這將是公號持續(xù)輸出更多優(yōu)質(zhì)文章的最強動力!