1. 程式人生 > >Python+appium+unittest UI自動化總結

Python+appium+unittest UI自動化總結

ride driver eps rri 實現 .so 測試 排序 number

什麽是UI自動化

自動化分層
  1. 單元自動化測試,指對軟件中最小可測試單元進行檢查和驗證,一般需要借助單元測試框架,如java的JUnit,python的unittest等
  2. 接口自動化測試,主要檢查驗證模塊間的調用返回以及不同系統、服務間的數據交換,常見的接口測試工具有postman、jmeter、loadrunner等;
  3. UI自動化測試,UI層是用戶使用產品的入口,所有功能通過這一層提供給用戶,測試工作大多集中在這一層,常見的測試工具有UFT、Robot Framework、Selenium、Appium等;

大部分公司只要求到第二層,及接口自動化測試,主要因為UI層經常改動,代碼可維護性弱,且如果需求經常變更時,對代碼邏輯也要經常改動。 但如果對於一些需求較為穩定,測試重復性工作多的使用UI自動化則能大量減少人力物力在一些簡單的手動重復工作上。

具體UI自動化實現

編程語言的選擇

python,是一門可讀性很強,很容易上手的編程語言,對於測試來說,可以在短時間內學會,並開始寫一下小程序。而且相對其Java來說,python可以用20行代碼完成Java100行代碼的功能,並且避免了重復造輪子。

自動化測試工具的選擇

appium,是一個開源的自動化測試工具,支持android、ios、mobile web、混合模式開發。在selenium的基礎上增加了對手機客戶端的特定操作,例如手勢操作和屏幕指向。

測試框架的選擇

unittest,是python的單元測試框架,使用unittest可以在有多個用例一起執行時,一個用例執行失敗,其他用例還能繼續執行。 且unittest引入了很多斷言,則測試過程中十分方便去判讀測試用例的執行失敗與否。

PageObject,是一種設計模式,一般使用在selenium自動化測試中。通過對頁面元素、操作的封裝,使得在後期對代碼的維護減少了很多冗余工作。

代碼框架

技術分享圖片

框架中主要是兩大塊,分別是result和testset,result用來存放執行用例後的html報告和日誌以及失敗時的截圖。

技術分享圖片

result 中主要以日期為文件夾,裏面文件為每次執行用例的測試報告及日誌,以及image文件夾,保存用例執行失敗時的截圖。

TestRunner

首先介紹testRunner,這是整個系統的運行的開始。

# -*- coding: utf-8 -*-
import threading
import unittest
from testSet.testcase.test_flight import Test_flight as testcase1
from testSet.testcase.test_test import test as testcase
import testSet.common.report as report
import testSet.page.basePage as basePage
from testSet.common.myServer import myServer
import time
from testSet.common.log import logger
import testSet.util.date as date

createReport = report.report() # 創建測試報告


class runTest():
    def __init__(self):
        pass

    def run(self, config, device):
        time.sleep(8)
        basePage.setconfig(config, device)  # 將設備號和端口號傳給basepage
        suite = unittest.TestLoader().loadTestsFromTestCase(testcase1)  # 將testcase1中的測試用例加入到測試集中
        runner = createReport.getReportConfig() 
        runner.run(suite)  # 開始執行測試集
        ms.quit()  # 退出appium服務

    def getDriver(self, driver):
        return driver


class myThread(threading.Thread):
    def __init__(self, device, config):
        threading.Thread.__init__(self)
        self.device = device
        self.config = config

    def run(self):
        if __name__ == ‘__main__‘:
            test = runTest()
            test.run(self.config, self.device)
            # test.driverquit()
            createReport.getfp().close()  # 關閉測試報告文件
            log = logger(date.today_report_path).getlog()
            log.info(self.device + "test over")

if __name__ == ‘__main__‘:
    try:
        devices = ["192.168.20.254:5555"]
        theading_pool = []
        for device in devices:   # 根據已連接的設備數,啟動多個線程
            ms = myServer(device)
            config = ms.run()
            t = myThread(device, config)  
            theading_pool.append(t)
        for t in theading_pool:
            t.start()
            time.sleep(5)
        for t in theading_pool:

            t.join()
    except:
        print("線程運行失敗")
        raise

testRunner包括runTest和myThead兩個類,myThead負責創建線程,runTest在線程中執行測試用例。

common

不具體說每個文件的作用及代碼了,舉例兩個比較重要的。

myServer

為了可以實現多設備並行測試,不能手動啟動appium客戶端後在執行用例,這樣只有1個設備分配到了appium的端口,也只能執行1個設備。因此需要用代碼實現啟動appium服務,並為不同的設備分配不同的端口。

import os
import unittest
from time import sleep
from .driver import driver
from selenium.common.exceptions import WebDriverException
import subprocess
import time
import urllib.request, urllib.error, urllib.parse
import random
import socket
from .log import logger
import testSet.util.date as date


# 啟動appium
class myServer(object):
    def __init__(self, device):
        # self.appiumPath = "D:\Appium"
        self.appiumPath = "F:\\Appium"
        self.device = device
        self.log = logger(date.today_report_path).getlog()

    def isOpen(self, ip, port):  # 判斷端口是否被占用
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            s.connect((ip, int(port)))
            s.shutdown(2)  # shutdown參數表示後續可否讀寫
            # print ‘%d is ok‘ % port
            return True
        except Exception as e:
            return False

    def getport(self):  # 獲得端口號
        port = random.randint(4700, 4900)
        # 判斷端口是否被占用
        while self.isOpen(‘127.0.0.1‘, port):
            port = random.randint(4700, 4900)
        return port

    def run(self):
        """
        啟動appium服務
        :return: aport 端口號
        """
        aport = self.getport()
        bport = self.getport()
        self.log.info("--------appium server start----------")
        # startCMD = "node D:\\Appium\\node_modules\\appium\\bin\\appium.js"
        # startCMD = "node Appium\\node_modules\\appium\\bin\\appium.js"
        cmd = ‘appium‘ + ‘ -p ‘ + str(aport) + ‘ --bootstrap-port ‘ + str(bport) + ‘ -U ‘ + str(self.device) + " --session-override"
        rootDirection = self.appiumPath[:2]  # 獲得appium服務所在的磁盤位置
        # 啟動appium
        # os.system(rootDirection + "&" + "cd" + self.appiumPath + "&" + startCMD)
        try:
            subprocess.Popen(rootDirection + "&" + "cd" + self.appiumPath + "&" + cmd, shell=True)  # 啟動appium服務
            return aport
        except Exception as msg:
            self.log.error(msg)
            raise

    def quit(self):
        """
        退出appium服務
        :return:
        """
        os.system(‘taskkill /f /im node.exe‘)
        self.log.info("----------------appium close---------------------")
driver

driver 負責連接手機,並啟動測試app

# -*- coding: utf-8 -*-

from appium import webdriver
from .log import logger
from . import report
import os
import testSet.util.date as date
import appium
from selenium.common.exceptions import WebDriverException
dr = webdriver.Remote


class driver(object):
    def __init__(self, device):
        self.device = device
        self.desired_caps ={}
        self.desired_caps[‘platformName‘] = ‘Android‘
        self.desired_caps[‘platformVersion‘] = ‘5.0.2‘
        self.desired_caps[‘udid‘] = self.device
        self.desired_caps[‘deviceName‘] = ‘hermes‘
        self.desired_caps[‘noReset‘] = True
        self.desired_caps[‘appPackage‘] = ‘com.igola.travel‘
        self.desired_caps[‘appActivity‘] = ‘com.igola.travel.ui.LaunchActivity‘
        self.log = logger(date.today_report_path).getlog()

    def connect(self, port):
        url = ‘http://localhost:%s/wd/hub‘ % str(port)
        self.log.debug(url)
        try:
            global dr
            dr = webdriver.Remote(url, self.desired_caps)
            self.log.debug("啟動接口為:%s,手機ID為:%s" % (str(port), self.device))
        except Exception:
            self.log.info("appium 啟動失敗")
            os.popen("taskkill /f /im adb.exe")
            raise

    def getDriver(self):
        return dr
report

使用htmlTestRunner生成html測試報告

# -*- coding: utf-8 -*-
import HTMLTestRunner
import time
import os
import testSet.util.date as date


class report:
    def __init__(self):
        self.runner = ""
        self.fp = ""
        self.sendReport()

    def sendReport(self):
        now = time.strftime("%Y-%m-%d-%H_%M_%S", time.localtime(time.time()))
        if not os.path.isdir(date.today_report_path):
            os.mkdir(date.today_report_path)
        report_abspath = os.path.join(date.today_report_path, now + ‘_report.html‘)
        self.fp = open(report_abspath, ‘wb‘)
        self.runner = HTMLTestRunner.HTMLTestRunner(
            stream=self.fp, title="appium自動化測試報告", description="用例執行結果:")

    def getReportConfig(self):
        return self.runner

    def getfp(self):
        return self.fp
測試用例

實現機票預訂流程

from ddt import ddt, data, unpack
import testSet.util.excel as excel
from . import testcase
import unittest
from testSet.common.sreenshot import screenshot
from testSet.page.homePage import homePage
from testSet.page.flightPage import FlightPage
from testSet.page.timelinePage import TimelinePage
from testSet.page.summaryPage import SummaryPage
from testSet.page.bookingPage import BookingPage
from testSet.page.bookingDetailPage import BookingDetailPage
from testSet.page.paymentPage import PaymentPage
from testSet.page.orderDetailPage import OrderDetailPage
from testSet.page.orderListPage import OrderListPage

Excel = excel.Excel("flight", "Sheet1")
isinit = False


@ddt
class Test_flight(testcase.Testcase):
    def setUp(self):
        super().setUp()
        self.cabin = ""

    @screenshot
    def step01_go_flightpage(self, expected_result):
        """
        跳轉到找飛機頁面
        """
        homePage().go_flightPage()

    @screenshot
    def step02_search(self, expected_result):
        """搜索跳轉
        """
        flight = FlightPage()
        self.assertTrue(flight.verify_page(), "找機票頁面進入錯誤")
        flight.select_ways(expected_result["type"])
        flight.select_cabin(expected_result["cabin"])
        FlightPage().search()

    @screenshot
    def step03_timeline(self, expected_result):
        """
        驗證timeline的航程詳情是否正確
        """
        timeline = TimelinePage()
        for type in range(0, int(expected_result["type"])):
            self.assertTrue(timeline.verify_page(), "timeline頁面進入錯誤")
            # actual_result = timeline.get_flight_info()
            # self.assertDictContainsSubset(actual_result, expected_result, "航程詳情錯誤")
            timeline.select_flight(expected_result["price"])

    @screenshot
    def step04_summary(self, expected_result):
        """
        驗證summary頁面的航程詳情是否正確
        :return:
        """
        summary = SummaryPage()
        self.assertTrue(summary.verify_page(), "summary頁面進入錯誤")
        summary.collapse()
        actual_result = summary.get_flight_info(expected_result["type"])
        trips = []
        for trip in expected_result.keys():
            if "trip_type" in trip:
                trips.append(expected_result[trip])
        leg_cabin = summary.check_cabin(expected_result["type"], *trips)
        if isinstance(leg_cabin, tuple):
            for key in leg_cabin[1].keys():
                actual_result[key] = leg_cabin[1][key]
            self.cabin = leg_cabin[0]
        else:
            self.cabin = leg_cabin
        self.assertDictContainsSubset(actual_result, expected_result, "航程詳情錯誤")
        summary.collapse()
        summary.select_ota()

    @screenshot
    def step05_go_booking(self, expected_result):
        """
        驗證booking航程詳情是否正確
        """
        booking = BookingPage()
        self.assertTrue(booking.verify_page(), "booking頁面進入錯誤")
        self.assertEqual(self.cabin, booking.check_cabin())
        booking.go_detail()

    @screenshot
    def step06_booking_detail(self, expected_result):
        booking_detail = BookingDetailPage()
        self.assertTrue(booking_detail.verify_page(), "booking航程詳情頁面進入錯誤")
        actual_result = booking_detail.get_flight_info(expected_result["type"])
        self.assertDictContainsSubset(actual_result, expected_result, "航程詳情錯誤")
        booking_detail.back_to_booking()

    @screenshot
    def step07_submit(self, expected_result):
        booking = BookingPage()
        booking.submit_order()

    @screenshot
    def step08_payment(self, expected_result):
        payment = PaymentPage()
        self.assertTrue(payment.verify_page(), "payment 頁面進入錯誤")
        self.assertEqual(self.cabin, payment.check_cabin())
        payment.pay_later()

    @screenshot
    def step09_go_order(self, expected_result):
        homePage().go_order()
        self.assertTrue(OrderListPage().verify_page(), "訂單列表頁面進入錯誤")
        OrderListPage().go_order_detail()

    @screenshot
    def step10_check_order(self, expected_result):
        detail = OrderDetailPage()
        self.assertTrue(detail.verify_page(), "訂單詳情頁面進入錯誤")
        detail.collapse()
        actual_result = detail.get_flight_info(expected_result["type"])
        self.assertDictContainsSubset(actual_result, expected_result, "航程詳情錯誤")

    def _steps(self):
        for name in sorted(dir(self)):
            if name.startswith("step"):
                yield name, getattr(self, name)

    @data(*Excel.next())
    def test_flights_detail(self, data):
        for name, step in self._steps():
            step(data)

Test_filght使用ddt,以數據為驅動,在excel中保存各種測試數據,一行測試數據則為一條用例,這樣可以避免測試用例的冗余,畢竟類似登錄就要測試各種情況。在測試用例中將每個步驟單獨分成一個函數,在函數前後都要使用斷言判斷是否執行成功,如果不成功則後面的步驟都不執行,直接跳出開始嚇一條用例的執行。

測試步驟中有數字01、02等,是為了確定測試的步驟順序,

def _steps(self):
        for name in sorted(dir(self)):
            if name.startswith("step"):
                yield name, getattr(self, name)

這一步則用來對測試用例中所有的以step開頭的函數進行排序,但需要註意的是,如果步驟超過10個,需要在1前面加上0,成為01,因為在函數名中,數字是以string格式保存的,排序時,會先比較第一位,再比較第二位,這樣執行順序可能會變成1,10,11...,2,3。所有需要在1前面加上0,這樣執行順序就正確了。

在測試用例中只顯示每一步操作,具體判斷邏輯代碼都交由每個頁面的具體實現代碼完成,即page文件夾中的文件。

而page文件也只執行相關業務判斷邏輯模塊,具體調用driver的手勢操作,例如click,swipe等,則交給basepage完成,page繼承於basepage。

Python+appium+unittest UI自動化總結