測開新手學自動化:分享幾點搭建自動化測試框架經驗


一、開頭說兩點

傳統軟件測試行業是以手工測試為主,也就是所謂的點點點,加上國內軟件公司不注重測試,受制於大環境影響等也就給了大眾一種測試人員雖然身處互聯網行業,卻是毫無技術可言的工種。

話鋒一轉,到了如今,不得不說一聲:大人,時代變了,最直觀的表現莫過於招聘要求的提高,越來越要求測試人員擁有七十二變的能力。而在這其中,自動化測試能力是現在手工測試邁向更高技術崗位的必經之路。

大家好,我是黎潘,我又來了,作為一名行業新手,我也是興致滿滿,選擇了當下較為火熱,且入門簡單的Python語言作為我邁向自動化測試工程師的重要幫手。所以以下討論的皆是與python相關的如何實現自動化的總結,當然肯定不止這一門語言可以實現,最好與實際項目需求和個人能力相結合,選擇最適合自己的自動化測試之路。

二、初識自動化測試

廣義上來講,自動化包括一切通過工具(程序)的方式來代替或輔助手工測試的行為都可以看作是自動化。狹義上來講,通過工具記錄或編寫腳本的方式模擬手工測試的過程,通過回放或運行腳本來執行測試用例,從而代替人工對系統的功能進行驗證。通俗易懂點就是一切能代替手工來執行測試用例,提高效率,不斷回歸的測試方法,在我眼里都能算是自動化測試。

2. 為什么要做自動化測試

2.1 減少手工測試占比

自動化測試可以替代大量的手工機械重復性操作,測試工程師可以把更多的時間花在更全面的用例設計新性功能的測試上。

2.2 提升回歸效率

自動化測試可以大幅提升回歸測試的效率,測試人員不用花費大量時間去校驗原有功能的正確性,最大的優點是非常適合敏捷開發過程中,也就是加入到CI/CD中。

2.3 持續測試系統的穩定

自動化測試可以高效實現某些手工測試無法完成或者代價巨大的測試類型。比如關鍵核心業務需要24小時持續運行的穩定性測試。

2.4 增加競爭力

隨着測試行業的發展,測試人們的發展方向越來越廣,技術方向越來越多樣化,更多的測試人傾向於往高技術攀爬。而擁有自動化測試的能力在以后很有可能是我們選擇工作的敲門磚了。雖然不少人都對這種變化感到惶恐不安,但是更多的人選擇站在狂風處,迎接挑戰,增加自身的競爭力,擁抱明天。

3. 什么項目適合自動化測試

3.1 需求穩定,不頻繁變更

測試腳本的穩定性決定了自動化測試的維護成本。如果軟件需求變動過於頻繁,測試人員需要根據變動的需求來更新測試用例以及相關的測試腳本,而腳本的維護本身就是一個代碼開發的過程,需要修改,調試,必要的時候還要修改自動化框架,如果花費的成本高於其節省的成本,那么自動化測試是失敗的。

我們可以優先對項目中核心模塊,相對穩定的模塊進行自動化,而變動較大的仍是用手工測試。

3.2 研發和維護周期長

由於自動化測試需求的確定,自動化測試框架的設計,測試腳本的編寫與調試均需要相當長的時間來完成。這樣的過程本身就是一個測試軟件地開發過程,需要較長的時間來完成。如果項目周期比較短,沒有足夠的時間去支持這樣一個過程,那么自動化測試便毫無意義。

3.3 項目資源足夠

自動化測試從需求范圍的確定,到自動化測試框架的設計,以及腳本的編寫與調試,均需要相當長的時間來完成。這樣的過程本身就是一個測試軟件的開發過程。因此有足夠的人力,物力非常重要。

三、搭建自己的接口測試框架

3.1 構建接口測試思維

當前互聯網產品最大的特點就是,上線周期通常是以"天"甚至是以"小時"為單位,而傳統軟件產品的周期多以"月",甚至以"年"為單位。因此,如何在保證產品質量下,有效縮短測試回歸時間成了重中之重。

兩個突破口:

  • 引入測試的並發執行,即從以往的串行執行測試用例,采用分布式的方法並行執行。
  • 從測試策略上找到突破口,從傳統軟件產品的金字塔測試策略往菱形測試策略轉變。以接口測試為主,GUI測試為輔,單元測試則根據公司實際情況進行。

四點建議:

  • 以中間層的API測試為重點做全面的測試
  • 輕量級的GUI測試,只覆蓋最核心直接影響主營業務流程的E2E場景
  • 最上層的GUI測試通常利用探索式測試思維,以人工測試的方式發現盡可能多的潛在問題
  • 單元測試只對那些相對穩定並且核心的服務和模塊開展全面的單元測試,而應用層或者上層業務只會做少量的

3.2 搭建自己的接口測試框架

3.2.1 為何要搭建自己的測試框架

  • 開發自己的框架更能結合自身工作中的痛點,難點來做一個針對性的解決,使其擴展性更高,后期也能接入CI/CD。
  • 利用現有工具來進行接口測試,隨着項目的規模變大,維護成本將會增大,不利於管控。
  • 工具本身具有一定的局限性,如支持的協議比較單一。
  • 不用糾結技術選型,根據自身的技術實力和技術功底來選擇,而不要以開發工程師的技術棧來選擇。

3.2.2 定義專屬框架目錄結構

  • test_case:存放測試用例
  • test_data:存放測試數據
  • report:存放測試報告
  • common:存放公共方法
  • lib:存放第三方庫
  • config:存放環境配置信息
  • main:框架主入口
  • fixture:類似unittest中的setUp/tearDown的存在,但功能遠比他們強大

3.2.3 構建框架流程

在框架構建過程中,由於篇符有限,本文只涉及其中部分環節。

1、在common公共模塊、封裝定義框架專屬的http請求能力

# !/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: pan-li
import requests


class HttpRequests(object):

    def __init__(self, url):
        self.url = url
        self.req = requests.session()
        # 自定義請求頭,根據自身所在公司項目需求
        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 enum


class URLConf(enum.Enum):
    TEST_URL = 'http://10.12.7.20:8443/v2x-omp/api/'

3、編寫接口測試用例在test_case文件中,第一版測試用例,安裝pytest,pip install -U pytest

import os
import sys
import pytest
import json
from 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):
        """查詢區域"""
        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):
        """查詢年檢總數"""
        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總數"""
        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, '查詢路測設備數量成功!'

    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、顯然前面的測試用例也是流水賬似的,還有很大的優化空間,現在就來一步一步進行。

5、優化一:利用feature特性優化前置和后置條件,fixture目錄下的v2x_fixture.py文件

import pytest
from common.http_requests import HttpRequests
from 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請求的調用,重新定義http()來進行調用。之前每次接口的調用都要附帶token參數,現在把獲取token的方法提取出來,單獨封裝,加上feature的裝飾,他會作用與每一個方法,用起來更加方便。此處的token是依賴登陸接口之后返回的值,可根據自身項目的需求封裝。

6、優化二: 為測試用例添加數據驅動模式

# 以第五個測試用例單獨為例
@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()裝飾器,第一個參數為參數名,后邊數組為測試數據,用例當中同樣添加形參deviceid"""

在 pytest 中,數據驅動是經由 pytest 自帶的 pytest.mark.parametrize() 來實現的。 pytest.mark.parametrize 是 pytest 的內置裝飾器,它允許你在 function 或者 class 上定義多組參 數和 fixture 來實現數據驅動。

**@pytest.mark.parametrize() ** 裝飾器接收兩個參數:

  • 第一個參數以字符串的形式存在,它代表能被被測試函數所能接受的參數,如果被測試函數有多個參數, 則以逗號分
  • 第二個參數用於保存測試數據。如果只有一組數據,以列表的形式存在,如果有多組數據,以列表嵌套元 組的形式存在

7、優化三: 為測試用例添加標簽,此時用到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即可
# 執行時pytest -m [標記名]

8、優化四: 配置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、優化五: 定制測試框架測試報告,屬於第三方應用放在lib目錄中

這里我們使用目前市面上使用人數較多的一款開源測試報告框架Allure,它支持絕大多數測試框架

安裝方法:

使用方法:

  • 執行pytest命令,並指定allure報告目錄: pytest -v -s test_v2x_api_02.py --alluredir=./allure_reports
  • 在線生成allure報告:allure serve allure_reports
  • 生成本地allure報告:allure generate allure_reports

當然這只是在控制台直接命令執行,還不夠方便,如果我們想在其他環境運行就又得配置環境變量,那么我們如何把它集成到我們的框架中呢

在共同方法中生成allure工具類,以便分辨運行環境是windows還是mac

import os
import sys
import 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模塊中,添加執行調度策略

import os
import threading
import pytest

from 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()  # 多線程先執行pytest命令生成測試報告
    run.join()
    gen.start()	# 報告生成后調用allure工具類生成本地報告

11、最后一版測試用例,整合前面的優化

import os
import sys
import json
from 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):
        """查詢區域"""
        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('查詢區域成功')

    def test_002_queryYearlyCheckCount(self, http, get_token):
        """查詢年檢總數"""
        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總數"""
        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'), '查詢路測設備數量成功!'
        # text = response.text
        # print(text)
        logger.info('查詢路側設備成功')
	
    # 簡單的數據驅動
    @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', ])

四、總結

關於這次自動化測試學習分享,涉及到的知識,只是冰山一角,參加狂師老師的全棧測開訓練營收獲非常大,還有很多的知識點沒有使用到,也就是我們的測試框架依然還有很多優化的空間,后續我會繼續,將細節補充到位,同時分享一些高階的用法。

原文出自發表於:公眾號:測試開發技術


免責聲明!

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



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