框架背景 前面我们已经介绍了Appium各种元素定位,手势操作、数据配置、Pageobject设计模式等等。但是前面的功能都是比较零散的,没有整体融合起来,实际项目实践过程中我们需要综合运用,那么本文我们将结合之前所学的内容,从0到1搭建一个完整的自动化测试框架。
框架功能
测试案例 测试环境
Win10 64Bit
Appium 1.7.2
考研帮App Android版3.1.0
夜神模拟器 Android 5.1.1
覆盖用例 1.登录场景
用户名
密码
自学网2018
zxw2018
自学网2017
zxw2017
666
222
2.注册场景
注册一个新的账号(账户和密码可以随机生成),完善院校和专业信息 (如:院校:上海-同济大学 专业:经济学类-统计学-经济统计学)
框架设计图
代码实现 driver配置封装 kyb_caps.yaml 配置表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 platformName: Android platformVersion: 5.1 .1 deviceName: 127.0 .0 .1 :62025 appname: kaoyan3.1.0.apk noReset: False unicodeKeyboard: True resetKeyboard: True appPackage: com.tal.kaoyan appActivity: com.tal.kaoyan.ui.activity.SplashActivity ip: 127.0 .0 .1 port: 4723
desired_caps.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import yamlimport logging.configfrom appium import webdriverimport osCON_LOG = '../config/log.conf' logging.config.fileConfig(CON_LOG) logging = logging.getLogger() def appium_desired (): with open ('../config/kyb_caps.yaml' ,'r' ,encoding='utf-8' ) as file: data = yaml.load(file) desired_caps={} desired_caps['platformName' ]=data['platformName' ] desired_caps['platformVersion' ]=data['platformVersion' ] desired_caps['deviceName' ]=data['deviceName' ] base_dir = os.path.dirname(os.path.dirname(__file__)) app_path = os.path.join(base_dir, 'app' , data['appname' ]) desired_caps['app' ] = app_path desired_caps['noReset' ]=data['noReset' ] desired_caps['unicodeKeyboard' ]=data['unicodeKeyboard' ] desired_caps['resetKeyboard' ]=data['resetKeyboard' ] desired_caps['appPackage' ]=data['appPackage' ] desired_caps['appActivity' ]=data['appActivity' ] logging.info('start run app...' ) driver = webdriver.Remote('http://' +str (data['ip' ])+':' +str (data['port' ])+'/wd/hub' , desired_caps) driver.implicitly_wait(5 ) return driver if __name__ == '__main__' : appium_desired()
基类封装 baseView.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class BaseView (object ): def __init__ (self,driver ): self.driver=driver def find_element (self,*loc ): return self.driver.find_element(*loc) def find_elements (self,*loc ): return self.driver.find_elements(*loc) def get_window_size (self ): return self.driver.get_window_size() def swipe (self,start_x, start_y, end_x, end_y, duration ): return self.driver.swipe(start_x, start_y, end_x, end_y, duration)
common公共模块封装 公共方法封装 : common_fun.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 from baseView.baseView import BaseViewfrom common.desired_caps import appium_desiredfrom selenium.common.exceptions import NoSuchElementExceptionimport logging.configfrom selenium.webdriver.common.by import Byimport osimport timeimport csvclass Common (BaseView ): cancel_upgradeBtn=(By.ID,'android:id/button2' ) skipBtn=(By.ID,'com.tal.kaoyan:id/tv_skip' ) wemedia_cacel=(By.ID, 'com.tal.kaoyan:id/view_wemedia_cacel' ) def check_updateBtn (self ): logging.info("============check_updateBtn===============" ) try : element = self.driver.find_element(*self.cancel_upgradeBtn) except NoSuchElementException: logging.info('update element is not found!' ) else : logging.info('click cancelBtn' ) element.click() def check_skipBtn (self ): logging.info("==========check_skipBtn===========" ) try : element = self.driver.find_element(*self.skipBtn) except NoSuchElementException: logging.info('skipBtn element is not found!' ) else : logging.info('click skipBtn' ) element.click() def get_screenSize (self ): ''' 获取屏幕尺寸 :return: ''' x = self.get_window_size()['width' ] y = self.get_window_size()['height' ] return (x, y) def swipeLeft (self ): logging.info('swipeLeft' ) l = self.get_screenSize() y1 = int (l[1 ] * 0.5 ) x1 = int (l[0 ] * 0.95 ) x2 = int (l[0 ] * 0.25 ) self.swipe(x1, y1, x2, y1, 1000 ) def getTime (self ): self.now = time.strftime("%Y-%m-%d %H_%M_%S" ) return self.now def getScreenShot (self, module ): time = self.getTime() image_file= os.path.dirname(os.path.dirname(__file__)) + '/screenshots/%s_%s.png' % (module, time) logging.info('get %s screenshot' % module) self.driver.get_screenshot_as_file(image_file) def check_market_ad (self ): '''检测登录或者注册之后的界面浮窗广告''' logging.info('=======check_market_ad=============' ) try : element=self.driver.find_element(*self.wemedia_cacel) except NoSuchElementException: pass else : logging.info('close market ad' ) element.click() def get_csv_data (self,csv_file,line ): ''' 获取csv文件指定行的数据 :param csv_file: csv文件路径 :param line: 数据行数 :return: ''' with open (csv_file, 'r' , encoding='utf-8-sig' ) as file: reader=csv.reader(file) for index, row in enumerate (reader,1 ): if index == line: return row if __name__ == '__main__' : driver=appium_desired()
业务模块封装 登录页面业务逻辑模块 loginView.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 import loggingfrom common.desired_caps import appium_desiredfrom common.common_fun import Common,Byfrom selenium.common.exceptions import NoSuchElementExceptionclass LoginView (Common ): username_type=(By.ID,'com.tal.kaoyan:id/login_email_edittext' ) password_type=(By.ID,'com.tal.kaoyan:id/login_password_edittext' ) loginBtn=(By.ID,'com.tal.kaoyan:id/login_login_btn' ) username=(By.ID,'com.tal.kaoyan:id/activity_usercenter_username' ) button_myself=(By.ID,'com.tal.kaoyan:id/mainactivity_button_mysefl' ) commitBtn = (By.ID, 'com.tal.kaoyan:id/tip_commit' ) settingBtn = (By.ID, 'com.tal.kaoyan:id/myapptitle_RightButtonWraper' ) logoutBtn=(By.ID,'com.tal.kaoyan:id/setting_logout_text' ) tip_commit=(By.ID,'com.tal.kaoyan:id/tip_commit' ) def login_action (self,username,password ): self.check_updateBtn() self.check_skipBtn() logging.info('============login_action==============' ) logging.info('username is:%s' % username) self.driver.find_element(*self.username_type).send_keys(username) logging.info('password is:%s' % password) self.driver.find_element(*self.password_type).send_keys(password) logging.info('click loginBtn' ) self.driver.find_element(*self.loginBtn).click() logging.info('login finished!' ) def check_account_alert (self ): '''检测账户登录后是否有账户下线提示''' logging.info('====check_account_alert======' ) try : element = self.driver.find_element(*self.commitBtn) except NoSuchElementException: pass else : logging.info('click commitBtn' ) element.click() def check_loginStatus (self ): logging.info('==========check_loginStatus===========' ) self.check_market_ad() self.check_account_alert() try : self.driver.find_element(*self.button_myself).click() self.driver.find_element(*self.username) except NoSuchElementException: logging.error('login Fail!' ) self.getScreenShot('login Fail' ) return False else : logging.info('login success!' ) self.logout_action() return True def logout_action (self ): logging.info('=========logout_action==========' ) self.driver.find_element(*self.settingBtn).click() self.driver.find_element(*self.logoutBtn).click() self.driver.find_element(*self.tip_commit).click() if __name__ == '__main__' : driver=appium_desired() l=LoginView(driver) l.login_action('自学网2018' ,'zxw2018' ) l.check_loginStatus()
注册页面业务逻辑封装 registerView.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 import loggingfrom common.desired_caps import appium_desiredfrom common.common_fun import Common,By,NoSuchElementExceptionimport randomclass RegisterView (Common ): register_text=(By.ID,'com.tal.kaoyan:id/login_register_text' ) userheader=(By.ID,'com.tal.kaoyan:id/activity_register_userheader' ) item_image=(By.ID,'com.tal.kaoyan:id/item_image' ) saveBtn=(By.ID,'com.tal.kaoyan:id/save' ) register_username=(By.ID,'com.tal.kaoyan:id/activity_register_username_edittext' ) register_password=(By.ID,'com.tal.kaoyan:id/activity_register_password_edittext' ) register_email=(By.ID,'com.tal.kaoyan:id/activity_register_email_edittext' ) register_btn=(By.ID,'com.tal.kaoyan:id/activity_register_register_btn' ) perfectinfomation_school=(By.ID,'com.tal.kaoyan:id/perfectinfomation_edit_school_name' ) perfectinfomation_major=(By.ID,'com.tal.kaoyan:id/activity_perfectinfomation_major' ) perfectinfomation_goBtn=(By.ID,'com.tal.kaoyan:id/activity_perfectinfomation_goBtn' ) forum_title=(By.ID,'com.tal.kaoyan:id/more_forum_title' ) university=(By.ID,'com.tal.kaoyan:id/university_search_item_name' ) major_subject_title= (By.ID, 'com.tal.kaoyan:id/major_subject_title' ) major_group_title= (By.ID, 'com.tal.kaoyan:id/major_group_title' ) major_search_item_name= (By.ID, 'com.tal.kaoyan:id/major_search_item_name' ) username = (By.ID, 'com.tal.kaoyan:id/activity_usercenter_username' ) button_myself = (By.ID, 'com.tal.kaoyan:id/mainactivity_button_mysefl' ) def register_action (self,register_username,register_password,register_email ): self.check_cancelBtn() self.check_skipBtn() logging.info('=========register_action===========' ) self.driver.find_element(*self.register_text).click() logging.info('set userheader' ) self.driver.find_element(*self.userheader).click() self.driver.find_elements(*self.item_image)[10 ].click() self.driver.find_element(*self.saveBtn).click() logging.info('register username is %s' %register_username) self.driver.find_element(*self.register_username).send_keys(register_username) logging.info('register_password is %s' %register_password) self.driver.find_element(*self.register_password).send_keys(register_password) logging.info('register_email is %s' %register_email) self.driver.find_element(*self.register_email).send_keys(register_email) logging.info('click register button' ) self.driver.find_element(*self.register_btn).click() try : self.driver.find_element(*self.perfectinfomation_school) except NoSuchElementException: logging.error('register Fail!' ) self.getScreenShot('register Fail' ) return False else : self.add_register_info() if self.check_registerStatus(): return True else : return False def add_register_info (self ): logging.info('===========add_register_info===========' ) logging.info("select school..." ) self.driver.find_element(*self.perfectinfomation_school).click() self.driver.find_elements(*self.forum_title)[1 ].click() self.driver.find_elements(*self.university)[1 ].click() logging.info("select major..." ) self.driver.find_element(*self.perfectinfomation_major).click() self.driver.find_elements(*self.major_subject_title)[1 ].click() self.driver.find_elements(*self.major_group_title)[2 ].click() self.driver.find_elements(*self.major_search_item_name)[1 ].click() self.driver.find_element(*self.perfectinfomation_goBtn).click() def check_register_status (self ): self.check_market_ad() logging.info('==========check_registerStatus===========' ) try : self.driver.find_element(*self.button_myself).click() self.driver.find_element(*self.username) except NoSuchElementException: logging.error('register Fail!' ) self.getScreenShot('register_Fail' ) return False else : logging.info('register success!' ) self.getScreenShot('register_success' ) return True if __name__ == '__main__' : driver=appium_desired() register=RegisterView(driver) username='zxw2018' +'FLY' +str (random.randint(1000 ,9000 )) password='zxw' +str (random.randint(1000 ,9000 )) email='51zxw' +str (random.randint(1000 ,9000 ))+'@163.com' register.register_action(username,password,email)
data数据封装 使用背景 在实际项目过程中,我们的数据可能是存储在一个数据文件中,如txt,excel、csv文件类型。我们可以封装一些方法来读取文件中的数据来实现数据驱动。
案例 将测试账号存储在account.csv文件,内容如下:
自学网2017
zxw2017
自学网2018
zxw2018
666
222
enumerate()简介 enumerate()是python的内置函数
enumerate在字典上是枚举、列举的意思
对于一个可迭代的(iterable)/可遍历的对象(如列表、字符串),enumerate将其组成一个索引序列,利用它可以同时获得索引和值
enumerate多用于在for循环中得到计数。
enumerate()使用 如果对一个列表,既要遍历索引又要遍历元素时,首先可以这样写:
1 2 3 4 5 6 7 8 9 10 list = ["这", "是", "一个", "测试","数据"] for i in range(len(list)): print(i,list[i]) >>> 0 这 1 是 2 一个 3 测试 4 数据
上述方法有些累赘,利用enumerate()会更加直接和优美:
1 2 3 4 5 6 7 8 9 list1 = ["这", "是", "一个", "测试","数据"] for index, item in enumerate(list1): print(index,item) >>> 0 这 1 是 2 一个 3 测试 4 数据
数据读取方法封装 1 2 3 4 5 6 7 8 9 10 11 12 import csv def get_csv_data (csv_file,line ): with open (csv_file, 'r' , encoding='utf-8-sig' ) as file: reader=csv.reader(file) for index, row in enumerate (reader,1 ): if index == line: return row csv_file='../data/account.csv' data=get_csv_data(csv_file,3 ) print (data)
utf-8与utf-8-sig两种编码格式的区别
UTF-8以字节为编码单元,它的字节顺序在所有系统中都是一样的,没有字节序的问题,也因此它实际上并不需要BOM(“ByteOrder Mark”)。但是UTF-8 with BOM即utf-8-sig需要提供BOM。
config文件配置 日志文件配置 log.config
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 [loggers] keys=root,infoLogger [logger_root] level=DEBUG handlers=consoleHandler,fileHandler [logger_infoLogger] handlers=consoleHandler,fileHandler qualname=infoLogger propagate=0 [handlers] keys=consoleHandler,fileHandler [handler_consoleHandler] class=StreamHandler level=INFO formatter=form02 args=(sys.stderr,) [handler_fileHandler] class=FileHandler level=INFO formatter=form01 args=('../logs/runlog.log', 'a') [formatters] keys=form01,form02 [formatter_form01] format=%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s [formatter_form02] format=%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s
测试用例封装 1.测试用例执行开始结束操作封装 myunit.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import unittestfrom common.desired_caps import appium_desiredimport loggingfrom time import sleepclass StartEnd (unittest.TestCase): def setUp (self ): logging.info('======setUp=========' ) self.driver=appium_desired() def tearDown (self ): logging.info('======tearDown=====' ) sleep(5 ) self.driver.close_app()
2.注册用例:test_register.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from common.myunit import StartEndfrom businessView.registerView import RegisterViewimport loggingimport randomimport unittestclass RegisterTest (StartEnd ): def test_user_register (self ): logging.info('=========test_user_register======' ) r=RegisterView(self.driver) username = 'zxw2018' + 'FLY' + str (random.randint(1000 , 9000 )) password = 'zxw' + str (random.randint(1000 , 9000 )) email = '51zxw' + str (random.randint(1000 , 9000 )) + '@163.com' self.assertTrue(r.register_action(username, password, email)) if __name__ == '__main__' : unittest.main()
3.登录用例:test_login.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 from common.myunit import StartEndfrom businessView.loginView import LoginViewimport unittestimport loggingclass LoginTest (StartEnd ): csv_file = '../data/account.csv' def test_login_zxw2017 (self ): logging.info('==========test_login_zxw2017========' ) l=LoginView(self.driver) data = l.get_csv_data(self.csv_file,1 ) l.login_action(data[0 ],data[1 ]) self.assertTrue(l.check_loginStatus()) def test_login_zxw2018 (self ): logging.info('=========test_login_zxw2018============' ) l=LoginView(self.driver) data = l.get_csv_data(self.csv_file,2 ) l.login_action(data[0 ],data[1 ]) self.assertTrue(l.check_loginStatus()) def test_login_erro (self ): logging.info('=======test_login_erro=========' ) l=LoginView(self.driver) data = l.get_csv_data(self.csv_file, 3 ) l.login_action(data[0 ], data[1 ]) self.assertTrue(l.check_loginStatus(),msg='login fail!' ) if __name__ == '__main__' : unittest.main()
执行测试用例&报告生成 BSTestRunner下载地址
run.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import unittestfrom BSTestRunner import BSTestRunnerimport timeimport loggingtest_dir = '../test_case' report_dir = '../reports' discover = unittest.defaultTestLoader.discover(test_dir, pattern='test_login.py' ) now = time.strftime("%Y-%m-%d %H_%M_%S" ) report_name = report_dir + '/' + now + ' test_report.html' with open (report_name, 'wb' ) as f: runner = BSTestRunner(stream=f, title="Kyb Test Report" , description="kyb Andriod app Test Report" ) logging.info("start run testcase..." ) runner.run(discover)
注意:
pattern参数可以控制运行不同模块的用例,如下所示表示运行指定路径以test开头的模块
1 discover = unittest.defaultTestLoader.discover(test_dir, pattern='test*.py' )
Bat批处理执行测试 前面脚本开发阶段我们都是使用pycharm IDE工具来运行脚本,但是当我们的脚本开发完成后,还每次打开IDE来执行自动化测试就不合理了,因为不仅每次打开比较麻烦,而且pycharm内存资源占用比较“感人”!这样非常影响执行效率。 针对这种情况,我们可以使用cmd命令或者封装为bat批处理脚本来运行。
启动appium服务 start_appium.bat
@echo off 为关闭“回显”,让命令行界面显得整洁一些。
执行测试用例 run.bat
1 2 3 4 5 @echo off d: cd D:\kyb_testProject\test_run C:\Python35\python.exe run.py pause
注意事项: 1.执行之前需要在run.py脚本添加如下内容:
1 2 3 import syspath='D:\\kyb_testProject\\' sys.path.append(path)
项目在IDE(Pycharm)中运行和我们在cmd中运行的路径是不一样的,在pycharm中运行时, 会默认pycharm的目录+我们的工程所在目录为运行目录。
而在cmd中运行时,会以我们的工程目录所在目录来运行。在import包时会首先从pythonPATH的环境变量中来查看包,如果没有你的PYTHONPATH中所包含的目录没有工程目录的根目录,那么你在导入不是同一个目录下的其他工程中的包时会出现import错误。
2.以上脚本编码格式必须为utf-8
自动化测试平台 前面我们已经开发完测试脚本,也使用bat批处理来封装了启动Appium服务和运行测试用例。但是还是不够自动化,比如我想每天下班时自动跑一下用例,或者当研发打了新包后自动开始运行测试脚本测试新包,那么该如实现呢?
持续集成(Continuous integration) 持续集成是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。
Jenkins简介 Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开放易用的软件平台,使软件的持续集成变成可能。
下载与安装 下载地址:https://jenkins.io/download/
下载后安装到指定的路径即可,默认启动页面为localhots:8080,如果8080端口被占用无法打开,可以进入到jenkins安装目录,找到==jenkins.xml==配置文件打开,修改如下代码的端口号即可。
1 <arguments>-Xrs -Xmx256m -Dhudson.lifecycle=hudson.lifecycle.WindowsServiceLifecycle -jar "%BASE%\jenkins.war" --httpPort=8080 --webroot="%BASE%\war"</arguments>
构建触发器
触发远程构建:如果您想通过访问一个特殊的预定义URL来触发新构建,请启用此选项。
Build after other projects are built:在其他项目触发的时候触发,里面有分为三种情况,也就是其他项目构建成功、失败、或者不稳定的时候触发项目;
Build periodically 定时构建
GitHub hook trigger for GITScm polling,根源Git的源码更新来触发构建
Poll SCM:定时检查源码变更(根据SCM软件的版本号),如果有更新就checkout最新code下来,然后执行构建动作。如下图配置:
1 */5 * * * * (每5分钟检查一次源码变化)
jenkins定时构建语法
(五颗星,中间用空格隔开)
第一个*表示分钟,取值0~59
第二个*表示小时,取值0~23
第三个*表示一个月的第几天,取值1~31
第四个*表示第几月,取值1~12
第五个*表示一周中的第几天,取值0~7,其中0和7代表的都是周日 使用案例 每天下午下班前18点定时构建一次
每天早上8点构建一次
每30分钟构建一次:
补充资料:Python邮件发送
报错相关
log配置路径错误
解决方案:需要以执行模块作为基点,取log配置文件的相对路径,然后配置到log配置文件中,handler_fileHandler
的args
路径里面,如下所示
1 2 3 4 5 [handler_fileHandler] class=FileHandler level=INFO formatter=form01 args=('../logs/runlog.log', 'a')
执行完成后app未及时关闭
1 2 selenium.common.exceptions.WebDriverException: Message: An unknown server-side error occurred while processing the command. Original error: Could not proxy command to remote server. Original error: Error: read ECONNRESET
用例执行完成后未关闭app导致需要加上如下代码:
参考资料
https://blog.csdn.net/vernice/article/details/46873169
https://blog.csdn.net/churximi/article/details/51648388
https://www.cnblogs.com/robert-zhang/p/9060365.html
https://baike.baidu.com/item/Jenkins/10917210?fr=aladdin
https://baike.baidu.com/item/持续集成/6250744
https://www.cnblogs.com/caoj/p/7815820.html
https://github.com/appium/appium/issues/11538