Airtest Project自定義啟動器支持批量運(yùn)行腳本,并兼容在AirtestIDE中使用

Python v3.7.0 / Airtest: 1.1.1 / PocoUI: 1.0.78
其他筆記:
Airtest Project 自定義啟動器批量運(yùn)行腳本
解決運(yùn)行Airtest腳本時(shí)opencv-contrib-python報(bào)錯(cuò)的問題
Airtest Project + Jenkins 微信小程序UI自動化測試 持續(xù)集成實(shí)踐

自定義的啟動器主要實(shí)現(xiàn)了以下功能:

  • 將一些公共參數(shù)和方法添加到全局變量中,在各業(yè)務(wù)腳本中無需聲明,可直接使用,如語句超時(shí)時(shí)間 TIMEOUT,就在此進(jìn)行統(tǒng)一設(shè)置;
  • 設(shè)置Airtest全局屬性值,對所有腳本生效,如下所示:
 ST.THRESHOLD = 0.80  # 圖像識別精確度閾值 [0,1]
 ST.THRESHOLD_STRICT = 0.85  # assert語句里圖像識別時(shí)使用的高要求閾值 [0,1]
 ST.OPDELAY = 1  # 每一步操作后等待多長時(shí)間再進(jìn)行下一步操作
 ST.FIND_TIMEOUT = 10  # 圖像查找超時(shí)時(shí)間,默認(rèn)為20s
 ST.CVSTRATEGY = ["tpl", "sift", "brisk"]  # 修改圖像識別算法順序,只要成功匹配任意一個(gè)符合設(shè)定闕值的結(jié)果,程序就會認(rèn)為識別成功
  • 在正式腳本運(yùn)行前后,添加子腳本的運(yùn)行,使得運(yùn)行各個(gè)業(yè)務(wù)腳本時(shí),初始頁面都將為小程序首頁,不需要再添加額外的環(huán)境判斷代碼;
    setup.air:在每個(gè)腳本運(yùn)行前,都會首先運(yùn)行此腳本。此腳本會判斷當(dāng)前頁面是否位于小程序首頁,若不是,則會重啟微信,進(jìn)入小程序首頁;
    teardown.air:在每個(gè)腳本運(yùn)行結(jié)束后,都會運(yùn)行此腳本。此腳本會點(diǎn)擊頁面頂部的HOME圖標(biāo),返回到小程序首頁。若成功返回到小程序首頁,則會嘗試關(guān)閉首頁遮罩廣告(注意:遮罩廣告樣式不能改變),若未能成功返回首頁,則會重啟微信,再進(jìn)入小程序首頁。

  • 通過配置config.csv中的內(nèi)容,實(shí)現(xiàn)批量運(yùn)行腳本;
    在運(yùn)行腳本時(shí),會先迭代查找suite/目錄下所有以.air結(jié)尾的目錄,自動忽略setup.airteardown.air,然后讀取config.csv中所配置的 Label 為 "Y"的腳本名稱,二者做交集運(yùn)算,其結(jié)果作為本次實(shí)際要運(yùn)行的腳本集合。

  • 批量運(yùn)行腳本結(jié)束后,支持生成聚合報(bào)告,與Jenkins進(jìn)行持續(xù)集成時(shí),此報(bào)告會作為附件進(jìn)行發(fā)送。點(diǎn)擊聚合報(bào)告中模塊名稱,會跳轉(zhuǎn)到Airtest Project自帶的測試報(bào)告頁面。


Airtest Project自帶的測試報(bào)告中,引用了很多靜態(tài)資源,如js/css文件、頁面截圖等,這里通過使用Nginx來搭建靜態(tài)資源服務(wù)器,使測試報(bào)告可以正常加載。

  • 兼容在AirtestIDE中使用自定義啟動器
    持續(xù)集成時(shí),必然要通過命令行來運(yùn)行業(yè)務(wù)腳本,需要手動傳入一些參數(shù)如設(shè)備號,腳本名等,而使用AirtesIDE運(yùn)行腳本時(shí),這些參數(shù)會自動傳入。源碼中使用argparse的方法來解析命令行傳入的參數(shù),為了兼容在AirtestIDE中使用自定義的啟動器,此處不能使用和源碼相同的方法來解析命令行參數(shù),否則AirtestIDE中會無法識別參數(shù),而導(dǎo)致報(bào)錯(cuò);
import sys

if len(sys.argv) == 1:  # 如果沒有傳入任何命令行參數(shù),則批量執(zhí)行腳本
    main()
    quit()
else:  # 在AirtestIDE中單個(gè)運(yùn)行腳本時(shí),會傳入script、log、device等參數(shù),則調(diào)用以下原始方法;
    ap = runner_parser()
    args = ap.parse_args()
    run_script(args, McCustomAirtestCase)

直接貼項(xiàng)目原始代碼,注釋已經(jīng)寫得很詳細(xì)了,有疑問的話歡迎在評論區(qū)留言。

#!/usr/bin/env python
# -*- coding:utf8 -*-
"""
@File  : mc_launcher.py
@Author: Rethink
@Date  : 2019/12/20 10:49
@Desc  :
"""
import csv
import datetime
import os
import shutil
import sys
import time
from argparse import *
from pathlib import Path
from typing import List

import airtest.report.report as report
import jinja2
from airtest.cli.parser import runner_parser
from airtest.cli.runner import run_script
from airtest.core.settings import Settings as ST

# 若腳本在IDE中運(yùn)行,IDE會自動幫忙加載AirtestCase;若用命令行運(yùn)行腳本,則需要導(dǎo)入 AirtestCase
if not globals().get("AirtestCase"):
    from airtest.cli.runner import AirtestCase

AIRTEST_EXPORT_DIR = os.getenv('AIRTEST_EXPORT_DIR')  # 測試報(bào)告相關(guān)資源打包后的導(dǎo)出路徑,目錄不存在會自動創(chuàng)建
AUTOLINE_HOST = os.getenv('AUTOLINE_HOST')  # 靜態(tài)資源文件服務(wù)器 格式:Scheme://IP:Port

if Path(AIRTEST_EXPORT_DIR).is_dir():
    pass
else:
    os.makedirs(AIRTEST_EXPORT_DIR)

class McCustomAirtestCase(AirtestCase):
    """
    Aietest Project自定義啟動器,參考文檔:http://airtest.netease.com/docs/cn/7_settings/3_script_record_features.html
    """

    PROJECT_ROOT = os.getenv("AIRTEST_PROJECT_ROOT", r"E:\treasure\Airtest\suite")  # 設(shè)置子腳本存放的根目錄

    def setUp(self):
        print("----------Custom Setup [Hook method]----------")
        # 將自定義的公共變量加入到`self.scope`中,在腳本代碼中就可以直接使用
        self.scope["SLEEPTIME"] = 1  # 睡眠時(shí)間
        self.scope["TIMEOUT"] = 5  # 超時(shí)時(shí)間

        # 設(shè)置`Airtest`全局屬性值
        ST.THRESHOLD = 0.80  # 圖像識別精確度閾值 [0,1]
        ST.THRESHOLD_STRICT = 0.85  # assert語句里圖像識別時(shí)使用的高要求閾值 [0,1]
        ST.OPDELAY = 2  # 每一步操作后等待多長時(shí)間進(jìn)行下一步操作, 只針對Airtest語句有效, 默認(rèn)0.1s
        ST.FIND_TIMEOUT = 10  # 圖像查找超時(shí)時(shí)間,默認(rèn)為20s
        ST.CVSTRATEGY = ["tpl", "sift", "brisk"]  # 修改圖像識別算法順序,只要成功匹配任意一個(gè)符合設(shè)定闕值的結(jié)果,程序就會認(rèn)為識別成功

        # 可以將一些通用的操作進(jìn)行封裝,然后在其他腳本中 import;
        # Airtest 提供了 using 接口,能夠?qū)⑿枰玫哪_本加入 sys.path 里,其中包含的圖片文件也會被加入 Template 的搜索路徑中
        # using("common.air")    # 相對于PROJECT_ROOT的路徑
        self.exec_other_script("setup.air")
        super(McCustomAirtestCase, self).setUp()

    def tearDown(self):
        print("----------Custom Teardown [Hook method]----------")
        self.exec_other_script("teardown.air")
        super(McCustomAirtestCase, self).tearDown()


def find_all_scripts(suite_dir: str = "") -> list:
    """
    遍歷suite目錄,取出所有的測試腳本
    """
    suite = []

    if not suite_dir:
        suite_dir = McCustomAirtestCase.PROJECT_ROOT

    for fpath in Path(suite_dir).iterdir():
        tmp = Path(suite_dir, fpath)
        if not tmp.is_dir():
            pass
        else:
            if fpath.suffix == '.air' and fpath.stem not in ["setup", "teardown"]:  # 這里會排除掉初始化腳本
                suite.append(fpath.name)
            else:
                deep_scripts = find_all_scripts(tmp)  # 遞歸遍歷
                suite += deep_scripts

    return suite


def allow_run_scripts() -> List[str]:
    """
    讀取配置文件,返回允許運(yùn)行的腳本名稱
    """
    config_allow_run = []

    with open("config.csv", "r") as f:
        f_csv = csv.DictReader(f)
        for row in f_csv:
            if row["Label"].upper() == "Y":
                config_allow_run.append(row["Script"].strip())

    return config_allow_run


def run_airtest(script, log_root, device=""):
    """
    運(yùn)行單個(gè)腳本,并生成測試報(bào)告,返回運(yùn)行結(jié)果
    :param script:  *.air, 要運(yùn)行的腳本
    :param device:  設(shè)備字符串
    :param log_root:  腳本日志存放目錄
    """
    if log_root.is_dir():
        shutil.rmtree(log_root)
    else:
        os.makedirs(log_root)
        print(str(log_root) + '>>> is created')

    # 組裝運(yùn)行參數(shù)
    args = Namespace(device=device,  # 設(shè)備字符串
                     log=log_root,  # log目錄
                     recording=None,  # 禁止錄屏
                     script=script  # *.air
                     )
    run_script(args, McCustomAirtestCase)


def generate_report(script, *, log_root, export_root):
    """
    生成測試報(bào)告
    :param script:  運(yùn)行名稱
    :param log_root:  腳本log目錄
    :return: export_root  測試報(bào)告輸出目錄
    """
    # 測試報(bào)告導(dǎo)出目錄
    if not export_root.is_dir():
        os.makedirs(export_root)
        print(str(export_root) + '>>> is created')

    output_file = Path(export_root, script.replace('.air', '.log'), 'log.html')  # 測試報(bào)告`log.html`存放路徑

    # 生成測試報(bào)告
    rpt = report.LogToHtml(script_root=script,  # *.air
                           log_root=log_root,  # log目錄
                           export_dir=export_root,  # 設(shè)置此參數(shù)后,生成的報(bào)告內(nèi)資源引用均使用相對路徑
                           lang='zh',  # 設(shè)置語言, 默認(rèn)"en"
                           script_name=script.replace(".air", ".py"),  # *.air/*.py
                           static_root=AUTOLINE_HOST + '/static',  # 設(shè)置此參數(shù)后,打包后的資源目錄中不會包含樣式資源
                           plugins=['poco.utils.airtest.report']  # 使報(bào)告支持poco語句
                           )
    rpt.report(template_name="log_template.html", output_file=output_file)

    # 提取腳本運(yùn)行結(jié)果
    result = rpt.test_result  # True or False

    return result


def summary_html(results: list, output_dir: str, template_dir: str, elapsed_time):
    """
    生成自定義的聚合報(bào)告
    :param results:  用例執(zhí)行結(jié)果
    :param output_dir:  html輸出目錄
    :param template_parent:  jinja2模板所在目錄
    """
    env = jinja2.Environment(
        loader=jinja2.FileSystemLoader(template_dir),
        extensions=(),
        autoescape=True,
    )
    template = env.get_template("summary_template.html", template_dir)
    html = template.render({"results": results,  # 運(yùn)行結(jié)果
                            "now": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),  # 當(dāng)前時(shí)間
                            "elapsed_time": elapsed_time,  # 腳本運(yùn)行時(shí)長
                            })
    output_file = Path(output_dir, "summary.html")  # 聚合報(bào)告路徑
    with open(output_file, 'w', encoding="utf-8") as f:
        f.write(html)


def copy_lastest_report(source):
    """
    復(fù)制最新生成的測試報(bào)告到 lastest/
    """
    latest = Path(AIRTEST_EXPORT_DIR, 'latest')
    if latest.is_dir():
        shutil.rmtree(latest)  # dst 目錄必須不能存在,否則copytree報(bào)錯(cuò)

    shutil.copytree(os.path.abspath(source),
                    os.path.abspath(latest))  # 目錄內(nèi)文件不能正在使用 否則無法復(fù)制成功PermissionError: [WinError 5] 拒絕訪問


def print_message(*message):
    print("*" * 50)
    mes = " ".join(map(str, message))
    print(mes)
    print("*" * 50)


def clear_history_report(report_dir, critical_day):
    """
    清除生成的歷史測試報(bào)告
    :param report_dir:  測試報(bào)告目錄
    :param critical_day:
    """
    root = Path(report_dir)
    reports = [report for report in root.iterdir() if report.is_dir() and report.name != 'latest']
    for report in reports:
        dt = datetime.datetime.strptime(report.name, '%Y%m%d%H%M%S')
        delta = datetime.datetime.now() - dt
        if delta.days > int(critical_day):
            shutil.rmtree(os.path.abspath(report))


def main():
    start_time = int(time.time())
    all_scripts = find_all_scripts()
    config_allow_run = allow_run_scripts()
    suite: set = set(all_scripts).intersection(config_allow_run)
    print_message("本次要運(yùn)行的用例集合為:", suite)

    results = []  # 腳本運(yùn)行結(jié)果匯總
    dt = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
    export_root = Path(AIRTEST_EXPORT_DIR, dt)  # 測試報(bào)告導(dǎo)出目錄

    for script in suite:
        print_message("正在運(yùn)行的用例名稱:" + script)

        log_root = Path(McCustomAirtestCase.PROJECT_ROOT, 'log', script.replace('.air', '.log'))  # 腳本日志目錄
        run_airtest(script, log_root)  # 運(yùn)行腳本
        result = generate_report(script, log_root=log_root, export_root=export_root)  # 生成測試報(bào)告
        results.append((script, result))

        if not result:
            print_message("用例執(zhí)行失敗: " + script)
        else:
            print_message("用例執(zhí)行成功:" + script)

    print_message("用例運(yùn)行結(jié)果匯總:", results)
    end_time = int(time.time())
    elapsed_time = end_time - start_time
    summary_html(results, output_dir=export_root, template_dir=McCustomAirtestCase.PROJECT_ROOT,
                 elapsed_time=elapsed_time)
    copy_lastest_report(source=export_root)
    clear_history_report(AIRTEST_EXPORT_DIR, 3)


if __name__ == '__main__':
    """
    通過命令行來啟動air腳本時(shí),需要傳入一些參數(shù)如設(shè)備號,腳本名等,而使用AirtesIDE運(yùn)行腳本時(shí),這些參數(shù)會自動傳入;
    源碼中使用argparse的方法來解析命令行參數(shù),此處不能使用和源碼相同的方法,否則AirtestIDE中會無法識別參數(shù),而導(dǎo)致報(bào)錯(cuò);
    具體源碼,詳見runner_parser、parse_args和run_script三個(gè)方法
    """
    # parser = argparse.ArgumentParser()
    # parser.add_argument("-b", "--Batch", type=str, help="是否啟用批量運(yùn)行腳本功能,為了支持在AirtestIDE中使用自定義的啟動器,此項(xiàng)默認(rèn)不啟用")
    # args = parser.parse_args()
    #
    # # 如果傳入Batch參數(shù)且其值為true, 則表示開啟批量運(yùn)行腳本功能
    # if args.Batch and args.Batch.lower() == "true":
    #     main()
    # else:
    #     # AirtestIDE中運(yùn)行腳本時(shí), 會自動帶入script, --device, --log, --recording參數(shù);
    #     ap = runner_parser()
    #     args = ap.parse_args()
    #     run_script(args, McCustomAirtestCase)

    if len(sys.argv) == 1:  # 如果沒有傳入任何命令行參數(shù),則批量執(zhí)行腳本
        main()
        quit()
    else:  # 在AirtestIDE中運(yùn)行腳本時(shí),會傳入script、log、device等參數(shù),則調(diào)用以下方法;
        ap = runner_parser()
        args = ap.parse_args()
        run_script(args, McCustomAirtestCase)

參考文檔

  1. 官方文檔: 7.3 腳本撰寫的高級特性
  2. UI 自動化測試工具 AirTest 學(xué)習(xí)筆記之自定義啟動器 · TesterHome
  3. 使用Airtest批量執(zhí)行案例腳本并聚合報(bào)告的方法 - u010127154的博客 - CSDN博客
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容