appium学习(四)--appium自动化测试框架综合实践
框架背景
前面我们已经学习了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: 127.0.0.1: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: 127.0.0.1 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']
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()
# 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 selenium.common.exceptions import NoSuchElementException
import logging.config
from selenium.webdriver.common.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):
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()
# 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 common.common_fun import Common,By
from selenium.common.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()
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!')
l.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
import logging
from common.desired_caps import appium_desired
from common.common_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()
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()使用
如果对一个列表,既要遍历索引又要遍历元素时,首先可以这样写:
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
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):
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
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):
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())
# @unittest.skip('skip test_login_zxw2018')
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())
# @unittest.skip("test_login_erro")
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()
执行测试用例&报告生成
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")
logging.info("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* ** *

浙公网安备 33010602011771号