什么是UI自動化
自動化分層
- 單元自動化測試,指對軟件中最小可測試單元進行檢查和驗證,一般需要借助單元測試框架,如java的JUnit,python的unittest等
- 接口自動化測試,主要檢查驗證模塊間的調用返回以及不同系統、服務間的數據交換,常見的接口測試工具有postman、jmeter、loadrunner等;
- 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