自动化框架搭建 python+appium

框架功能

 业务功能的封装

 测试用例封装

 测试包管理

 截图处理

 断言处理

 日志获取

 测试报告生成

 数据驱动

 数据配置

测试案例

测试环境

Mac

Appium v1.18.3

app软件

华为mate30

覆盖用例

1.登录场景

2.下单场景

框架设计图


代码实现

driver配置封装

kyb_caps.yaml 配置表

# 测试平台 iOSAndroid

platformName: Android

# 连接模拟器

# 平台版本

#platformVersion: 6.0.1

# 设备名字

deviceName: HUAIWEI Mate 30 Pro 5G

# 连接真机

platformVersion: '10.0'

uuid: *************3142


#便于后期维护,可以只修改appname

appname: ********debug.apk(包名)

# 支持中文输入

unicodeKeyboard: True

# 重置输入法到原有状态

resetKeyboard: True

# 控制是否清除session信息

noReset: False

# app的包

appPackage: com.**********

# app activity

appActivity: com.**************.******Activity

# appium serverIPport

ip: 127.0.0.1

port: 4723

desired_caps.py文件

from appium import webdriver

import yaml

import logging

import logging.config

import os

CON_LOG='../config/log.conf'

logging.config.fileConfig(CON_LOG)

logging=logging.getLogger()

def appium_desired():

    with open('../config/********caps.yaml', 'r', encoding='utf-8') as file:

        data = yaml.load(file,Loader = yaml.FullLoader)

    desired_caps={}

    desired_caps['platformName'] = data['platformName']

    desired_caps['platformVersion'] = data['platformVersion']

    desired_caps['deviceName'] = data['deviceName']

    desired_caps['uuid'] = data['uuid']

    base_dir = os.path.dirname(os.path.dirname(__file__))

    app_path = os.path.join(base_dir, 'appname', data['appname'])

    desired_caps['appname'] = app_path

    desired_caps['appPackage'] = data['appPackage']

    desired_caps['appActivity'] = data['appActivity']

    desired_caps['noReset'] = data['noReset']

    desired_caps['unicodeKeyboard'] = data['unicodeKeyboard']

    desired_caps['resetKeyboard'] = data['resetKeyboard']

    logging.info('start app...')

    # driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)

    driver = webdriver.Remote('http://' + str(data['ip']) + ':' + str(data['port']) + '/wd/hub',desired_caps)

    return driver

if __name__ == '__main__':

    appium_desired()

相对路径符号含义

“.”表示当前目录

“..” 表示当前目录的上一级目录。

“./”表示当前目录下的某个文件或文件夹,视后面跟着的名字而定

“../”表示当前目录上一级目录的文件或文件夹,视后面跟着的名字而定。

基类封装

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.baseViews import BaseView

from common.desired_caps import appium_desired

from selenium.common.exceptions import NoSuchElementException

import logging

from selenium.webdriver.common.by import By

import time,os

import csv

class Common(BaseView):

#当前时间

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)

    print(image_file)

    logging.info('get %s screenshot' %module)

    self.driver.get_screenshot_as_file(image_file)

#读取csv文件

def get_csv_data(self,csv_file,line):

    logging.info('========get_csv_data=========')

    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

#获取屏幕尺寸

def get_size(self):

    x =self.driver.get_window_size()['width']

    y =self.driver.get_window_size()['height']

    return x, y


if __name__ == '__main__':

    driver = appium_desired()

    com = Common(driver)

    com.swipeLeft()

    com.getScreenShot(str('startapp'))

业务模块封装

1.登录页面业务逻辑模块

loginView.py

import logging

from common.commin_fun import Common,NoSuchElementException

from common.desired_caps import appium_desired

from selenium.webdriver.common.by import By

from page.loginpage import LoginPage

import time

#登录

class LoginView(Common):

    # 跳转登录

    logBtn = (By.ID, '*********:id/t*******e')

    # 用户名

    username_type = (By.XPATH,'/**************************ditText')

    # 密码

    password_type = (By.XPATH,'/*****************itText')

    # 登录按钮

    loginBtn = (By.ID, 'com.**********:id/bt****gin')

    # homepage = (By.ID,'com.***********:id/iv**nt')

    tip_commit = (By.ID, 'com.***********:id/tv_****dits')

    def login_action(self,username,password):

        logging.info('click logBtn')

        self.driver.find_element(*self.logBtn).click()

        logging.info('click logBtn finished')

        time.sleep(3)

        logging.info('=========login_action=======')

        logging.info('username is:%s' %username)

       self.driver.find_element(*self.username_type).send_keys(username)

        logging.info('usernameinput finished')

        time.sleep(3)

        logging.info('=========login_action=======')

        logging.info('password is:%s' %password)

       self.driver.find_element(*self.password_type).send_keys(password)

        logging.info('passwordinput finished')

        time.sleep(3)

        logging.info('click loginBtn')

        self.driver.find_element(*self.loginBtn).click()

        logging.info('login loginBtn finished')

        time.sleep(3)

    def check_account_alert(self):

        logging.info('============check_account_alert===========')

        try:

            element = self.driver.find_element(self.tip_commit)

        except NoSuchElementException:

            pass

        else:

            logging.info('Logged in')

    def check_login_staus(self):

        logging.info('=======check_login_staus========')

        # self.check_market_ad()

        self.check_account_alert()

        try:

            self.driver.find_element(*self.tip_commit)

        except NoSuchElementException:

            logging.error('login Fail')

            self.getScreenShot('login Fail')

            return False

        else:

            logging.info('===========login success==============')

            return True

if __name__ == '__main__':

    driver = appium_desired()

    l = LoginView(driver)

    l.login_action('1584********692','q******************/an******View')

    #跳过动画

    skip_animation = (By.ID,'com.*********:id/skip')

    #跳过弹窗

    skip_popup = (By.ID,'com.**********:id/tv_ok')

    #首页

    homepage = (By.ID, 'com.*******:id/iv*****ent')


    bj_cbd = (By.ID,'com.*************:id/ite****mg')


    reserve_room = (By.XPATH,'/***********************out')

    #同意

    agree = (By.ID,'com.**********:id/c*********s')

    #提交订单

    submit = (By.ID,'com.***************:id/b************it')

    #立即支付

    immediate_payment = (By.ID,'com.**************id/b*******y')

    #账户余额

    account_balance = (By.ID,'com.***************id/c***********nt')

    #确认支付

    confirm_payment = (By.ID,'com.*********************id/b*******y')

    def Yizhan_xiadan(self):

        logging.info('click homepage')

        self.driver.find_element(*self.homepage).click()

        logging.info('login homepage finished')

        time.sleep(3)

        logging.info('=========click  yizhan_homepage=========')

        self.driver.find_element(*self.yizhan_homepage).click()

        logging.info('==============click yizhan_homepage finished===============')

        time.sleep(3)

        logging.info('=========click  skip_animation=========')

        self.driver.find_element(*self.skip_animation).click()

        logging.info('==============click skip_animation finished===============')

        time.sleep(3)

        logging.info('=========click  skip_popup=========')

        self.driver.find_element(*self.skip_popup).click()

        logging.info('==============click skip_popup finished===============')

        time.sleep(3)

        logging.info('============swipe up=============')

        self.swipe(868,1735,868,757,500)

        logging.info('============swipe up  finished=============')

        logging.info('==============cilck  bj_cbd=================')

        self.driver.find_element(*self.bj_cbd).click()

        logging.info('==============bj_cbd  finished===============')

        logging.info('============swipe up two=============')

        self.swipe(835, 1857, 835, 776, 696)

        logging.info('============swipe up two finished=============')

        logging.info('==================click reserve_room==============')

        self.driver.find_element(*self.reserve_room).click()

        logging.info('============reserve_room finished=============')

        logging.info('==================click agree==============')

        self.driver.find_element(*self.agree).click()

        logging.info('============agree finished=============')

        logging.info('==================click submit==============')

        self.driver.find_element(*self.submit).click()

        logging.info('============submit finished=============')

        logging.info('==================click immediate_payment==============')

        self.driver.find_element(*self.immediate_payment).click()

        logging.info('============immediate_payment finished=============')

        logging.info('==================click account_balance==============')

        self.driver.find_element(*self.account_balance).click()

        logging.info('============account_balance finished=============')

        logging.info('==================click confirm_payment==============')

        self.driver.find_element(*self.confirm_payment).click()

        logging.info('============confirm_payment finished=============')

data数据封装

使用背景

在实际项目过程中,我们的数据可能是存储在一个数据文件中,如txt,excel、csv文件类型。我们可以封装一些方法来读取文件中的数据来实现数据驱动。

案例

将测试账号存储在account.csv文件,内容如下:

130*******745q******

enumerate()简介

enumerate()是python的内置函数

enumerate在字典上是枚举、列举的意思

对于一个可迭代的(iterable)/可遍历的对象(如列表、字符串),enumerate将其组成一个索引序列,利用它可以同时获得索引和值

enumerate多用于在for循环中得到计数。

enumerate()使用

如果对一个列表,既要遍历索引又要遍历元素时,首先可以这样写:

list= ["这", "是", "一个", "测试","数据"]

    foriinrange(len(list)):

        print(i,list[i])

>>>

0 这

1 是

2 一个

3 测试

4 数据

上述方法有些累赘,利用enumerate()会更加直接和优美:

list1 = ["这", "是", "一个", "测试","数据"]

    forindex,iteminenumerate(list1):

        print(index,item)

>>>

0 这

1 是

2 一个

3 测试

4 数据

数据读取方法封装

import csv

def get_csv_data(self,csv_file,line):

    logging.info('========get_csv_data=========')

    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-8utf-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):

        logging.info('=========start============')

        self.driver = appium_desired()

    def tearDown(self):

        logging.info('===========end==========')

        sleep(5)

        self.driver.close_app()

2.登录和下单用例:test_login.py

from common.myunit import StartEnd

from businessView.loginVIew import LoginView

from businessView.homePage import HomePage

from businessView.misu_xiadan import YizhanXiadan

import logging,unittest

from BSTestRunner import BSTestRunner

import time

#登录

class LoginYest(StartEnd):

    csv_file ='../data/account.csv'

    def test_login_cg(self):

        logging.info('===============test_page===============')

        h = HomePage(self.driver)

        l = LoginView(self.driver)

        y = YizhanXiadan(self.driver)

        data = l.get_csv_data(self.csv_file, 1)

        h.get_into()

        l.login_action(data[0], data[1])

        self.assertTrue(l.check_login_staus(), msg=('login fail!!!'))

        time.sleep(3)

        y.Yizhan_xiadan()

if __name__ == '__main__':

    unittest.main()

执行测试用例&报告生成

BSTestRunner下载地址

run.py文件

from BSTestRunner import BSTestRunner

import unittest

import time,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='***测试报告',description='*******登录下单流程')

    logging.info('start run rest case')

    runner.run(discover)

注意:

pattern参数可以控制运行不同模块的用例,如下所示表示运行指定路径以test开头的模块

discover = unittest.defaultTestLoader.discover(test_dir, pattern='test*.py')

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容