自动化测试框架

背景

Ui 自动化测试框架

目标

  1. 确保质量
  2. 减少人工成本
  3. 统一标准

考虑方面

  1. 平台支持。
  2. 稳定性。
  3. 维护成本。
  4. 可扩展性。

All: Appium, kobiton, calabash, MonkeyTalk
Andoird: robotinum, uiautomator, espresso
iOS: XCTest, UIAutomation, Frank, KIF, Kiwi
Appium + cucumber .使用 cucumber BDD 框架,底层还是使用 appium 。为封装更多 step,可以使用 selenium-cucumber(待查看)。

支付宝开源了 SoloPi android 自动化测试工具。是依靠录制回放、性能测试、一机多控 来实现的。测试是通过录制用户操作,记录下来再在各个设备上回放。

行为驱动开发(Behavior-driven development,缩写BDD)是一种敏捷软件开发的技术。它通过用自然语言书写非程序员可读的测试用例扩展了测试驱动开发方法。
满足 BDD 和 跨平台是我们需要的。

Calabash 满足 BDD 和 跨平台。 相对小众,且 ios 接入 calsbash 成本太高。Calabash 对元素的定位主要依赖 ID,ios 一般不具备这个条件。

Appium 是一个C/S架构,核心是一个Web服务器,它提供了一套REST的接口。当收到客户端的连接后,就会监听到命令,然后在移动设备上执行这些命令,最后将执行结果放在HTTP响应中返还给客户端。
可以和 python 很好结合,但是在业务快速发展的过程中,维护成本会越来越难以接受

结合 Calabash 和 Appium,通过 appium + cucumber + selenium 来实现上层使用 cucumber 来满足 BDD,底层使用 appium 来确保跨平台和稳定性

搭建初始化

现在跑通的是 appium + cucumber + selenium + ruby

  1. ruby 安装
    mac 自带的 2.3.0 可能不够用,需要安装 2.4.0 以上的。
    可以直接 brew install ruby 安装,注意要设置环境变量取代默认版本.
  2. Cucumber.rb
    gem install cucumber
  3. s

开始

新建文件夹

mkdir TestDemo

初始化 cucumber。

cucumber –init
执行上面命令,会生成如下目录结构:

1
2
3
4
features # 存放feature的目录
├── step_definitions # 存放steps的目录
└── support # 环境配置
└── env.rb

创建 Gemfile 文件

创建 Gemfile 文件
touch Gemfile

打开Gemfile,导入Ruby库

1
2
3
4
5
6
7
8
9
10
11
source 'https://www.rubygems.org'

gem 'appium_lib', '~> 9.7.4'
gem 'rest-client', '~> 2.0.2'
gem 'rspec', '~> 3.6.0'
gem 'cucumber', '~> 2.4.0'
gem 'rspec-expectations', '~> 3.6.0'
gem 'spec', '~> 5.3.4'
gem 'sauce_whisk', '~> 0.0.13'
gem 'test-unit', '~> 2.5.5'
gem 'selenium-cucumber', '~> 3.1.5'

安装依赖库

1
2
3
4
5
# 需要先安装bundle
gem install bundle

# 安装ruby依赖
bundle install

配置运行基本信息

  1. 进入features/support目录,新建appium.txt文件
  2. 编辑appium.txt文件,这里只配置了iOS的模拟器和真正代码
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
[caps]
# 模拟器
#platformName = "ios"
#deviceName = "iPhone X"
#platformVersion = "11.2"
#app = "./apps/AutoUITestDemo.app"
#automationName = "XCUITest"
#noReset="true"

# 真机
# platformName = "ios"
# deviceName = "xxx"
# platformVersion = "10.3.3"
# app = "./apps/AutoUITestDemo.app"
# automationName = "XCUITest"
# udid = "xxxx"
# xcodeOrgId = "QT6N53BFV6"
# xcodeSigningId = "ZHH59G3WE3"
# autoAcceptAlerts = "true"
# waitForAppScript = "$.delay(5000); $.acceptAlert();" # 处理系统弹窗

# android
platformName = "android"
deviceName = "ce10171a72ba8938057e"
platformVersion = "8"
app = "./app/app-flavors_process_test-debug.apk"//app 路径,这是以 TestDemo 文件夹为相对路径
automationName = "UIAutomator2"
noSign="true"

[appium_lib]
sauce_username = false
sauce_access_key = false
  1. 打开env.rb文件,配置启动入口
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

require 'rspec/expectations'
require 'appium_lib'
require 'selenium-cucumber'
# require 'cucumber/ast'

# Create a custom World class so we don't pollute `Object` with Appium methods
class AppiumWorld
end

if ENV['IDEVICENAME']=='android'
caps = Appium.load_appium_txt file: File.expand_path("./../android/appium.txt", __FILE__), verbose: true
elsif ENV['IDEVICENAME']=='ios'
caps = Appium.load_appium_txt file: File.expand_path("./../ios/appium.txt", __FILE__), verbose: true
else
caps = Appium.load_appium_txt file: File.expand_path('./', __FILE__), verbose: true
end

# end
Appium::Driver.new(caps, true)
Appium.promote_appium_methods AppiumWorld

World do
AppiumWorld.new
end

Before { $driver.start_driver }
After { $driver.driver_quit }

features目录下,新建guide.feature文件,用来描述测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@guidepage
Feature: 引导页
1.首次安装应用,判断是否展示引导页;
滑到最后一张,判断是否展示“登录/注册”和“进入首页”两个按钮;
点击“登录/注册”按钮,判断是否展示登录界面。
2.滑动到最后一张引导页,点击“进入首页”按钮,判断引导页是否还存在。

@guide_01
Scenario: 首次安装应用,展示引导页;滑动到最后一张引导页,展示“登录/注册”和“进入首页”两个按钮
When 展示引导页
Then 滑动到最后一页
Then 展示“登录/注册”和“进入首页”两个按钮
When I press "“登录/注册"
Then I see "登陆"

@guide_02
Scenario: 点击最后一张引导页“进入首页”按钮,判断引导页是否还存在
When 滑动到最后一张引导页,点击“进入首页”按钮
Then 退出引导页

在step_definitions目录下,新建 common_steps.rb 和 guide.rb 文件,用来存放脚本代码

  • 编写rb脚本之前,先打开 appium,再执行 cucumber 命令。
  • 将 cucumber 提示我们实现的复制下来放到 rb 文件
  • common_steps.rb 可以放公共的可复用的 step,guide.rb 放自己的step
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
common_steps.rb 

When("I press {string}") do |string|
guideIsExist = exists { button(string) }
puts guideIsExist ? "存在该按钮" : "不存在该按钮"
expect(guideIsExist).to be true
sleep 1
end

guide.rb

# @guide_01
# 首次安装应用,判断是否展示引导页;
# 滑到最后一张,判断是否展示“登录/注册”和“进入首页”两个按钮;
# 点击“登录/注册”按钮,判断是否展示登录界面。
When("展示引导页") do
guideIsExist = exists { button("现在就下载") }
puts guideIsExist ? "存在引导页面" : "不存在引导页面"
expect(guideIsExist).to be true
end

Then(/^滑动到最后一页$/) do
swipe_to_last_guide_view
sleep(1)
end

Then(/^展示“登录\/注册”和“进入首页”两个按钮$/) do
$loginBtnIsExist = exists { button("现在就下载") }
puts $loginBtnIsExist ? "存在“登录/注册”按钮" : "不存在“登录/注册”按钮"
expect($loginBtnIsExist).to be true

startBtnIsExist = exists { id("setting_items") }
puts startBtnIsExist ? "存在“进入首页”按钮" : "不存在“进入首页”按钮"
expect(startBtnIsExist).to be true
end
  • 运行 cucumber –dry-run 来查看 rb 文件编写测试实例的情况(是否有遗漏)
  • 运行 cucumber。cucumber –tags @guidepage 可按 tags 运行。

补充

元素定位

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
# 1、使用button查找按钮
first_button // 查找第一个button
button(value) // 查找第一个包含value的button,返回[UIAButton|XCUIElementTypeButton]对象
buttons(value) // 查找所有包含value的所有buttons,返回[Array<UIAButton|XCUIElementTypeButton>]对象

eg:
button("登录") // 查找登录按钮

# 2、使用textfield查找输入框
first_textfield // 查找第一个textfield
textfield(value) // 查找第一个包含value的textfield,返回[TextField]

eg:
textfield("用户名") // 查找

# 3、使用accessibility_id查找
id(value) // 返回id等于value的元素

eg:
id("登录") // 返回登录按钮
id("登录页面") // 返回登录页面

# 4、通过find查找
find(value) // 返回包含value的元素
find_elements(:class, 'XCUIElementTypeCell') // 通过类名查找

eg:
find("登录页面")

# 5、通过xpath查找
xpath(xpath_str)

# web元素定位:
# 测试web页面首先需要切换driver的上下文
web = driver.available_contexts[1]
driver.set_context(web)

# 定位web页面的元素
driver.find_elements(:css, ".re-bb") # 通过类选择器.re-bb定位css的元素

更多详情

常用事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 通过坐标点击
tap(x: 68, y: 171)

// 通过按钮元素点击
button("登录").click

// 滑动手势
swipe(direction:, element: nil) // direction - Either 'up', 'down', 'left' or 'right'.

eg: 上滑手势
swipe(direction: "up", element: nil)

// wait
wait { find("登录页面") } // 等待登录页面加载完成

// sleep
sleep(2) // 延时2秒

更多详情

断言

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
# 1. 相等
expect(actual).to eq(expected) # passes if actual == expected
expect(actual).to eql(expected) # passes if actual.eql?(expected)
expect(actual).not_to eql(not_expected) # passes if not(actual.eql?(expected))

# 2、比较
expect(actual).to be > expected
expect(actual).to be >= expected
expect(actual).to be <= expected
expect(actual).to be < expected
expect(actual).to be_within(delta).of(expected)

# 3、类型判断
expect(actual).to be > expected
expect(actual).to be >= expected
expect(actual).to be <= expected
expect(actual).to be < expected
expect(actual).to be_within(delta).of(expected)

# 4、Bool值比较
expect(actual).to be_truthy # passes if actual is truthy (not nil or false)
expect(actual).to be true # passes if actual == true
expect(actual).to be_falsy # passes if actual is falsy (nil or false)
expect(actual).to be false # passes if actual == false
expect(actual).to be_nil # passes if actual is nil
expect(actual).to_not be_nil # passes if actual is not nil

# 5、错误
expect { ... }.to raise_error
expect { ... }.to raise_error(ErrorClass)
expect { ... }.to raise_error("message")
expect { ... }.to raise_error(ErrorClass, "message")

# 6、异常
expect { ... }.to throw_symbol
expect { ... }.to throw_symbol(:symbol)
expect { ... }.to throw_symbol(:symbol, 'value')

更多断言详情