Appium自动化测试框架综合实践x
时间:2020-11-08 13:05:37 来源:勤学考试网 本文已影响 人
框架背景
前面我们已经学习了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 配置表
platformName: Android
#模拟器
platformVersion: 5.1.1
deviceName: :62025
#mx4真机
#platformVersion: 5.1
#udid: 750BBKL22GDN
#deviceName: MX4
appname: kaoyan3.1.0.apk
noReset: False
unicodeKeyboard: True
resetKeyboard: True
appPackage: com.tal.kaoyan
appActivity: com.tal.kaoyan.ui.activity.SplashActivity
ip:
port: 4723
desired_caps.py
import yaml
import logging.config
from appium import webdriver
import os
CON_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']
('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()
# with open('../config/kyb_caps.yaml','r',encoding='utf-8') as file:
# data = yaml.load(file)
#base_dir = os.path.dirname(os.path.dirname(__file__))
#app_path = os.path.join(base_dir, 'app', data['appname'])
#print(app_path)
相对路径符号含义
“.”表示当前目录
“..” 表示当前目录的上一级目录。
“./”表示当前目录下的某个文件或文件夹,视后面跟着的名字而定
“../”表示当前目录上一级目录的文件或文件夹,视后面跟着的名字而定。
基类封装
baseView.py
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
from baseView.baseView import BaseView
from common.desired_caps import appium_desired
from mon.exceptions import NoSuchElementException
import logging.config
from mon.by import By
import os
import time
import csv
class 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):
("============check_updateBtn===============")
try:
element = self.driver.find_element(*self.cancel_upgradeBtn)
except NoSuchElementException:
('update element is not found!')
else:
('click cancelBtn')
element.click()
def check_skipBtn(self):
("==========check_skipBtn===========")
try:
element = self.driver.find_element(*self.skipBtn)
except NoSuchElementException:
('skipBtn element is not found!')
else:
('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):
('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)
('get %s screenshot' % module)
self.driver.get_screenshot_as_file(image_file)
def check_market_ad(self):
'''检测登录或者注册之后的界面浮窗广告'''
('=======check_market_ad=============')
try:
element=self.driver.find_element(*self.wemedia_cacel)
except NoSuchElementException:
pass
else:
('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()
# c=Common(driver)
# c.check_updateBtn()
# # c.check_skipBtn()
# c.swipeLef()
# c.swipeLef()
# c.getScreenShot("startApp")
业务模块封装
1.登录页面业务逻辑模块
loginView.py
import logging
from common.desired_caps import appium_desired
from mon_fun import Common,By
from mon.exceptions import NoSuchElementException
class 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()
('============login_action==============')
('username is:%s' % username)
self.driver.find_element(*self.username_type).send_keys(username)
('password is:%s' % password)
self.driver.find_element(*self.password_type).send_keys(password)
('click loginBtn')
self.driver.find_element(*self.loginBtn).click()
('login finished!')
def check_account_alert(self):
'''检测账户登录后是否有账户下线提示'''
('====check_account_alert======')
try:
element = self.driver.find_element(*mitBtn)
except NoSuchElementException:
pass
else:
('click commitBtn')
element.click()
def check_loginStatus(self):
('==========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:
('login success!')
l.logout_action()
return True
def logout_action(self):
('=========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
import logging
from common.desired_caps import appium_desired
from mon_fun import Common,By,NoSuchElementException
import random
class 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()
('=========register_action===========')
self.driver.find_element(*self.register_text).click()
#头像设置
('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()
#用户名密码填写
('register username is %s' %register_username)
self.driver.find_element(*self.register_username).send_keys(register_username)
('register_password is %s' %register_password)
self.driver.find_element(*self.register_password).send_keys(register_password)
('register_email is %s' %register_email)
self.driver.find_element(*self.register_email).send_keys(register_email)
('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):
('===========add_register_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()
#专业选择:经济学类-统计学-经济统计学
("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()
('==========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:
('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()使用
如果对一个列表,既要遍历索引又要遍历元素时,首先可以这样写:
list = ["这", "是", "一个", "测试","数据"]
for i in range(len(list)):
print(i,list[i])
>>>
0 这
1 是
2 一个
3 测试
4 数据
上述方法有些累赘,利用enumerate()会更加直接和优美:
list1 = ["这", "是", "一个", "测试","数据"]
for index, item in enumerate(list1):
print(index,item)
>>>
0 这
1 是
2 一个
3 测试
4 数据
数据读取方法封装
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
[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
import unittest
from common.desired_caps import appium_desired
import logging
from time import sleep
class StartEnd(unittest.TestCase):
def setUp(self):
('======setUp=========')
self.driver=appium_desired()
def tearDown(self):
('======tearDown=====')
sleep(5)
self.driver.close_app()
2.注册用例:test_register.py
from common.myunit import StartEnd
from businessView.registerView import RegisterView
import logging
import random
import unittest
class RegisterTest(StartEnd):
def test_user_register(self):
('=========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
from common.myunit import StartEnd
from businessView.loginView import LoginView
import unittest
import logging
class LoginTest(StartEnd):
csv_file = '../data/account.csv'
# @unittest.skip("test_login_zxw2017")
def test_login_zxw2017(self):
('==========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())
# @unittest.skip('skip test_login_zxw2018')
def test_login_zxw2018(self):
('=========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())
# @unittest.skip("test_login_erro")
def test_login_erro(self):
('=======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
import unittest
from BSTestRunner import BSTestRunner
import time
import logging
#指定测试用例和测试报告的路径
test_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")
("start run testcase...")
runner.run(discover)
注意:
pattern参数可以控制运行不同模块的用例,如下所示表示运行指定路径以test开头的模块
discover = unittest.defaultTestLoader.discover(test_dir, pattern='test*.py')
Bat批处理执行测试
前面脚本开发阶段我们都是使用pycharm IDE工具来运行脚本,但是当我们的脚本开发完成后,还每次打开IDE来执行自动化测试就不合理了,因为不仅每次打开比较麻烦,而且pycharm内存资源占用比较“感人”!这样非常影响执行效率。
针对这种情况,我们可以使用cmd命令或者封装为bat批处理脚本来运行。
启动appium服务
start_appium.bat
@echo off
appium
pause
@echo off 为关闭“回显”,让命令行界面显得整洁一些。
执行测试用例
run.bat
@echo off
d:
cd D:\kyb_testProject\test_run
C:\Python35\python.exe run.py
pause
注意事项:
1.执行之前需要在run.py脚本添加如下内容:
import sys
path='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配置文件打开,修改如下代码的端口号即可。
<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下来,然后执行构建动作。如下图配置:
*/5 * * * * (每5分钟检查一次源码变化)
jenkins定时构建语法
* * * * *
(五颗星,中间用空格隔开)
第一个*表示分钟,取值0~59
第二个*表示小时,取值0~23
第三个*表示一个月的第几天,取值1~31
第四个*表示第几月,取值1~12
第五个*表示一周中的第几天,取值0~7,其中0和7代表的都是周日
使用案例
每天下午下班前18点定时构建一次
0 18 * * *
每天早上8点构建一次
0 8 * * *
每30分钟构建一次:
H/30 * * * *
补充资料: Python邮件发送
参考资料
/vernice/article/details /churximi/article/details /robert-zhang/p/9060365.html
/item/Jenkinsfr=aladdin
/item/持续集成/6250744
/caoj/p/7815820.html