理想国

我要看尽这世间繁华


  • 首页

  • 归档

  • 标签

  • 分类

  • 关于

  • 搜索

使用 Jenkins 实现持续集成构建检查

发表于 2016-08-14 | 更新于 2019-04-03 | 分类于 Development

通过《使用Jenkins搭建iOS/Android持续集成打包平台》和《关于持续集成打包平台的Jenkins配置和构建脚本实现细节》两篇文章,我们已经在原理概念和实践操作两个层面掌握了如何搭建一个完整的持续集成打包平台。

不过,在实际使用过程中,发现有时候还会存在一个问题。研发同学提交新的代码后,Jenkins端可以成功执行构建,并生成安装包;然而在将安装包安装至移动设备时,却发现有时候会出现无法成功安装,或者安装后出现启动闪退的情况。

为了及时发现该类问题,我们还需要对每次构建生成的安装包进行检查。本文便是对构建检查涉及到的方法进行介绍。

构建生成.app

为了降低问题的复杂度,我们可以选择在模拟器中运行构建生成的安装包。之前在《从0到1搭建移动App功能自动化测试平台(1):模拟器中运行iOS应用》也讲解过,要在模拟器中运行iOS应用,需要在Xcode中编译时选择模拟器类型,并且编译生成的文件后缀为.app。

对应的,构建生成.app的命令如下:

1
2
3
4
5
6
7
# build .app file from source code
xcodebuild \ # xctool
-workspace ${WORKSPACE_PATH} \
-scheme ${SCHEME} \
-configuration ${CONFIGURATION} \
-sdk ${SDK} \
-derivedDataPath ${OUTPUT_FOLDER}

xcodebuild/xctool参数说明:

  • -workspace:需要打包的workspace,后面接的文件一定要是.xcworkspace结尾的;
  • -scheme:需要打包的Scheme,一般与$project_name相同;
  • -sdk:区分iphone device和Simulator,可通过xcodebuild -showsdks获取,例如iphonesimulator9.3。
  • -configuration:需要打包的配置文件,我们一般在项目中添加多个配置,适合不同的环境,Release/Debug;
  • -derivedDataPath:指定编译结果文件的存储路径;例如,指定-derivedDataPath build_outputs时,将在项目根目录下创建一个build_outputs文件夹,生成的.app文件将位于build_outputs/Build/Products/${CONFIGURATION}-iphoneos中。

同样地,这里也可以使用xctool代替xcodebuild。

这里使用到的命令和参数基本上和《关于持续集成打包平台的Jenkins配置和构建脚本实现细节》一文中的大致相同,唯一需要特别注意的是-sdk参数。因为是要在模拟器中运行,因此-sdk参数要设置为iphonesimulator,而非iphoneos。

命令成功执行后,就会在指定的${OUTPUT_FOLDER}目录中生成${SCHEME}.app文件,这也就是我们构建生成的产物。另外,熟悉iOS的同学都知道,.app文件其实是一个文件夹,为了实现更好的存储,我们也可以额外地再做一步操作,将.app文件夹压缩转换为.zip格式,而且Appium也是支持读取.zip格式的安装包的。

至此,适用于iOS模拟器运行的构建产物已准备就绪。这里涉及到的脚本实现已更新至【debugtalk/JenkinsTemplateForApp】。

实现构建检查

那要怎样对构建生成的产物进行检查呢?

最简单的方式,就是在iOS模拟器中运行构建生成的.app,并执行一组基本的自动化测试用例。在执行过程中,如果出现无法成功安装,或者安装成功后启动出现闪退,或者自动化测试用例执行失败等异常情况,则说明我们最新提交的代码存在问题,需要通知研发同学及时进行修复。

而这些实现方式,其实我在《从0到1搭建移动App功能自动化测试平台》系列文章中都已经进行了详细讲解,并形成了一套较为成熟的自动化测试框架,【debugtalk/AppiumBooster】。

在此基础上,我们无需再做其它工作,只需要按照debugtalk/AppiumBooster的要求在表格中编写一组基本的自动化测试用例,即可采用如下方式执行构建检查。

1
2
$ cd ${AppiumBooster_Folder}
$ ruby run.rb -p "${OUTPUT_FOLDER}/${SCHEME}.app.zip" --disable_output_color > test_result.log

在如上命令中,通过-p参数指定构建生成的安装包,然后就可以在iOS模拟器中运行事先编写好的自动化测试用例,从而实现构建检查。

在这里,我们还可以通过--disable_output_color开关将输出日志的颜色关闭。之所以实现这么一个功能,是因为在Jenkins中本来也无法显示颜色,但是如果还将Terminal中有颜色的日志内容输出到Jenkins中,就会出现一些额外的字符,比较影响日志的美观。

现在,我们已经分别实现了代码构建和构建检查这两个核心的操作环节,而要执行最终的持续集成,我们还需要做最后一项工作,即在Jenkins中将这两个环节串联起来。

Jenkins配置

关于Jenkins的相关基础概念、实施流程和配置细节,我在之前的文章中已经讲解得非常详细了。在此我就只进行一点补充。

要实现在构建完成后再运行一些额外的脚本,例如我们的构建检查命令,需要使用到Jenkins的一个插件,Post-Build Script Plug-in。

安装完该插件后,在Jenkins配置界面的Post-build Actions栏目中,Add post-build action选项列表中就会多出Execute a set of scripts选项。选择该项后,会出现如下配置界面。

Jenkins Post_build_Actions Execute_shell menu

选择Execute shell后,会出现一个文本框,然后我们就可以将构建检查的命令填写到里面。

Jenkins Post_build_Actions Execute_shell

在这里我们用到了${AppiumBooster_Folder}参数,该参数也需要通过String Parameter来进行定义,用于指定AppiumBooster项目的路径。

Jenkins String Parameter

最后,为了便于将执行自动化测试用例的日志和执行构建的日志分开,我们将执行自动化测试用例的日志写入到了test_result.log文件中。然后,在Archives build artifacts中就可以通过${AppiumBooster_Folder}/test_result.log将执行构建检查的日志收集起来,并展示到每次构建的页面中。

延续一贯的开箱即用原则,我将使用Jenkins实现持续集成构建检查涉及到的Jenkins配置也做成了一套模板,并更新到【debugtalk/JenkinsTemplateForApp】中了,供大家参考。

写在最后

至此,通过本系列的几篇文章,关于如何使用Jenkins实现移动APP持续集成的相关内容应该都已经覆盖得差不多了。

不过,由于我个人的近期工作主要集中在iOS部分,因此在讲解的过程中都是以iOS为主。后续在将工作重心移到Android部分后,我会再在DebugTalk的这几篇文章中更新Android部分的内容。

从0到1搭建移动App功能自动化测试平台 (4):自动化测试代码⎡工程化⎦

发表于 2016-07-28 | 更新于 2019-04-03 | 分类于 Testing , 自动化测试

在本系列的上一篇文章中,我通过系统登录这一典型功能点,演示了编写自动化测试脚本的整个流程,并对测试脚本进行了初步优化。

在本文中,我将重点介绍如何对自动化测试脚本实现⎡工程化⎦的组织和管理。

测试脚本⎡工程化⎦

首先说下什么是测试脚本的工程化。

通过之前的工作,我们已经可以让单个自动化测试用例正常运行起来了。然而,这还只算是一个demo,一切才刚刚开始。

试想,一个项目的自动化测试用例少则数百,多则成千上万。如何将这些自动化测试用例组织起来?如何实现更好的可重用机制?如何实现更好的可拓展机制?这些都还是我们当前的demo所不具备的,也是我们需要通过“工程化”手段进行改造的原因。

引入Minitest/RSpec

在Ruby中,说到测试首先就会想到Minitest或RSpec,这是Ruby中用的最多的两个测试框架。通过这些框架,我们可以很好地实现对Ruby测试用例的管理。

同样地,由于我们的自动化测试脚本是采用Ruby编写的,因此我们也可以使用Minitest/RSpec来管理我们的自动化测试用例。

基于该想法,我们采用RSpec对之前的系统登录测试用例进行工程结构初始化。对于熟悉Ruby编程,或者有一定代码基础的同学而言,很自然地,可以将测试用例框架初始化为如下结构。

1
2
3
4
5
6
7
8
9
10
├── Gemfile
├── android
│ └── appium.txt
├── common
│ ├── requires.rb
│ └── spec_helper.rb
└── ios
├── appium.txt
└── spec
└── login_spec.rb

在Gemfile中,指定了项目依赖的库。

1
2
3
4
5
6
# filename: Gemfile
source 'https://gems.ruby-china.org'

gem 'rspec'
gem 'appium_lib'
gem 'appium_console'

在common/spec_helper.rb中,定义了模拟器和RSpec初始化相关的代码。

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
# filename: common/spec_helper.rb

def setup_driver
return if $driver
appium_txt = File.join(Dir.pwd, 'ios', 'appium.txt')
caps = Appium.load_appium_txt file: appium_txt
Appium::Driver.new caps
end

def promote_methods
Appium.promote_appium_methods RSpec::Core::ExampleGroup
end

setup_driver
promote_methods

RSpec.configure do |config|

config.before(:each) do
$driver.start_driver
wait { alert_accept }
end

config.after(:each) do
driver_quit
end

end

在common/requires.rb中,实现了对相关库文件的引用。

1
2
3
4
5
6
7
8
# filename: common/requires.rb

# load lib
require 'rspec'
require 'appium_lib'

# setup rspec
require_relative 'spec_helper'

在ios/appium.txt中,对iOS模拟器信息和测试包路径进行了配置。

1
2
3
4
5
[caps]
platformName = "ios"
deviceName = "iPhone 6s"
platformVersion = "9.3"
app = "/Users/Leo/MyProjects/AppiumBooster/ios/app/test.app"

在ios/spec/目录中,则是测试用例的内容。例如,ios/spec/login_spec.rb对应的就是系统登录的测试用例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# filename: ios/spec/login_spec.rb
require_relative '../../common/requires'

describe 'Login' do

it 'with valid account' do
wait { id('btnMenuMyAccount').click }
wait { id 'uiviewMyAccount' }

wait { id('tablecellMyAccountLogin').click }
wait { id 'uiviewLogIn' }

wait { id('txtfieldEmailAddress').type 'leo.lee@debugtalk.com' }
wait { id('sectxtfieldPassword').type '123321' }
wait { id('btnLogin').click }
wait { id 'tablecellMyMessage' }
end

end

通过以上代码结构初始化,我们的测试用例框架的雏形就形成了。接下来,在Terminal中切换到项目根目录,然后通过rspec ios命令就可以执行ios目录中的测试用例了。

1
2
3
4
5
➜ rspec ios
.

Finished in 2 minutes 7.2 seconds (files took 1.76 seconds to load)
1 example, 0 failures

完整的代码请参考debugtalk/AppiumBooster的1.FirstTest分支。

添加第二条测试用例

现在,我们尝试往当前的测试框架中添加第二条测试用例。

例如,第二条测试用例要实现启动后从当前地区切换至中国。那么,就可以新增ios/spec/change_country_spec.rb。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# filename: ios/spec/change_country_spec.rb
require_relative '../../common/requires'

describe 'Change country' do

it 'from Hong Kong to China' do
wait { id('btnMenuMyAccount').click }
wait { id 'uiviewMyAccount' }

wait { id('tablecellMyAccountSystemSettings').click }
wait { id 'txtCountryDistrict' }

wait { id('txtCountryDistrict').click }
wait { id 'uiviewSelectCountry' }

wait { id('tablecellSelectCN').click }

wait { id('btnArrowLeft').click }
wait { id 'uiviewMyAccount' }
end

end

完整的代码请参考debugtalk/AppiumBooster的2.SecondTest分支。

现在我们凝视已经添加的两个测试用例,有发现什么问题么?

是的,重复代码太多。在每一步操作中,都要用id来定位控件,还要用wait来实现等待机制。

除此之外,当前代码最大的问题就是测试用例与控件映射杂糅在一起。造成的后果就是,不管是控件映射发生变动,还是测试用例需要修改,都要来修改这一份代码,维护难度较大。

重构:测试用例与控件映射分离

基于以上问题,我们首要的改造任务就是将测试用例与控件映射进行分离。

考虑到常用的控件操作方法就只有几个(click,type),因此我们可以将控件操作方法单独封装为一个模块,作为公共模块。

1
2
3
4
5
6
7
8
9
10
11
module Actions

def click
wait { @found_cell.click }
end

def type(text)
wait { @found_cell.type text }
end

end

然后,将APP中每一个页面封装为一个模块(module),将页面中的控件映射为模块的静态方法(method),并通过include机制引入方法模块。

例如,登录页面就可以封装为如下代码。

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
module Pages
module Login
class << self

include Actions

def field_Email_Address
@found_cell = wait { id 'txtfieldEmailAddress' }
self
end

def field_Password
@found_cell = wait { id 'sectxtfieldPassword' }
self
end

def button_Login
@found_cell = wait { id 'btnLogin' }
self
end

end
end
end

module Kernel
def login
Pages::Login
end
end

这里还用到了一点Ruby元编程技巧,就是将页面模块封装为一个方法,并加入到Kernel模块下。这样做的好处就是,我们可以在项目的任意地方直接通过login.button_Login.click这样的形式来对控件进行操作了。

完成以上改造后,系统登录测试用例就可以采用如下形式进行编写了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
describe 'Login' do

it 'with valid account' do
# switch to My Account page
my_account.button_My_Account.click
inner_screen.has_control 'uiviewMyAccount'

# enter login page
my_account.button_Login.click
inner_screen.has_control 'uiviewLogIn'

# login
login.field_Email_Address.type 'leo.lee@debugtalk.com'
login.field_Password.type '123321'
login.button_Login.click
inner_screen.has_control 'tablecellMyMessage'
end

end

完整的代码请参考debugtalk/AppiumBooster的3.RefactorV1分支。

To be continued …

经过这一轮重构,我们的测试用例与控件映射已经实现了分离,测试用例的可重用性与可扩展性也得到了极大的提升。

然而,在当前模式下,所有的测试用例仍然是以代码形式存在的,新增和修改测试用例时都需要到工程目录下编辑Ruby文件。

那有没有一种可能,我们只需要在表格中维护自动化测试用例(如下图),然后由代码来读取表格内容就可以自动执行测试呢?

AppiumBooster overview testcase examples

是的,这就是我们对测试框架进行⎡工程化⎦改造的下一个形态,也就是AppiumBooster现在的样子。

在下一篇文章中,我们再进行详细探讨。

关于持续集成打包平台的 Jenkins 配置和构建脚本实现细节

发表于 2016-07-26 | 更新于 2019-04-03 | 分类于 Development

在《使用Jenkins搭建iOS/Android持续集成打包平台》一文中,我对如何使用Jenkins搭建iOS/Android持续集成打包平台的基础概念和实施流程进行了介绍。本文作为配套,对搭建持续集成打包平台中涉及到的执行命令、构建脚本(build.py),以及Jenkins的配置进行详细的补充说明。

当然,如果你不关心技术实现细节,也可以完全不用理会,直接参照【开箱即用】部分按照步骤进行操作即可。

关于iOS的构建

对iOS源码进行构建,目标是要生成.ipa文件,即iOS应用安装包。

当前,构建方式主要包括两种:

  • 源码 -> .archive文件 -> .ipa文件
  • 源码 -> .app文件 -> .ipa文件

这两种方式的主要差异是生成的中间产物不同,对应的,两种构建方式采用的命令也不同。

源码 -> .archive -> .ipa

1
2
3
4
5
6
7
8
# build archive file from source code
xcodebuild \ # xctool
-workspace ${WORKSPACE_PATH} \
-scheme ${SCHEME} \
-configuration ${CONFIGURATION} \
-sdk ${SDK}
-archivePath ${archive_path}
archive

archive:对编译结果进行归档,会生成一个.xcarchive的文件,位于-archivePath指定的目录中。需要注意的是,对模拟器类型的sdk无法使用archive命令。

1
2
3
4
5
6
7
8
# export ipa file from .archive
xcodebuild -exportArchive \
-exportFormat format \
-archivePath xcarchivepath \
-exportPath destinationpath \
-exportProvisioningProfile profilename \
[-exportSigningIdentity identityname]
[-exportInstallerIdentity identityname]

源码 -> .app -> .ipa

1
2
3
4
5
6
7
# build .app file from source code
xcodebuild \ # xctool
-workspace ${WORKSPACE_PATH} \
-scheme ${SCHEME} \
-configuration ${CONFIGURATION} \
-sdk ${SDK}
-derivedDataPath ${OUTPUT_FOLDER}
1
2
3
4
5
6
# convert .app file to ipa file
xcrun \
-sdk iphoneos \
PackageApplication \
-v ${OUTPUT_FOLDER}/Release-iphoneos/xxx.app \
-o ${OUTPUT_FOLDER}/Release-iphoneos/xxx.ipa

参数说明

xcodebuild/xctool参数:

  • -workspace:需要打包的workspace,后面接的文件一定要是.xcworkspace结尾的;
  • -scheme:需要打包的Scheme,一般与$project_name相同;
  • -sdk:区分iphone device和Simulator,可通过xcodebuild -showsdks获取,例如iphoneos和iphonesimulator9.3;
  • -configuration:需要打包的配置文件,我们一般在项目中添加多个配置,适合不同的环境,Release/Debug;
  • -exportFormat:导出的格式,通常填写为ipa;
  • -archivePath:.xcarchive文件的路径;
  • -exportPath:导出文件(.ipa)的路径;
  • -exportProvisioningProfile:profile文件证书;
  • -derivedDataPath:指定编译结果文件的存储路径;例如,指定-derivedDataPath ${OUTPUT_FOLDER}时,将在项目根目录下创建一个${OUTPUT_FOLDER}文件夹,生成的.app文件将位于${OUTPUT_FOLDER}/Build/Products/${CONFIGURATION}-iphoneos中。

除了采用官方的xcodebuild命令,还可以使用由Facebook开发维护的xctool。xctool命令的使用方法基本与xcodebuild一致,但是输出的日志会清晰很多,而且还有许多其它优化,详情请参考xctool的官方文档。

xcrun参数:

  • -v:指定.app文件的路径
  • -o:指定生成.ipa文件的路径

补充说明

1、获取Targets、Schemes、Configurations参数

在填写target/workspace/scheme/configuration等参数时,如果不知道该怎么填写,可以在项目根目录下执行xcodebuild -list命令,它会列出当前项目的所有可选参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  Store_iOS git:(NPED) ✗ xcodebuild -list
Information about project "Store":
Targets:
Store
StoreCI

Build Configurations:
Debug
Release

If no build configuration is specified and -scheme is not passed then "Release" is used.

Schemes:
Store
StoreCI

2、清除缓存文件

在每次build之后,工程目录下会遗留一些缓存文件,以便下次build时减少编译时间。然而,若因为工程配置错误等问题造成编译失败后,下次再编译时就可能会受到缓存的影响。

因此,在持续集成构建脚本中,比较好的做法是在每次build之前都清理一下上一次编译遗留的缓存文件。

1
2
3
4
5
6
# clean before build
xctool \
-workspace ${WORKSPACE_PATH} \
-scheme ${SCHEME} \
-configuration ${CONFIGURATION} \
clean

clean:清除编译产生的问题,下次编译就是全新的编译了

3、处理Cocoapod依赖库

另外一个需要注意的是,若项目是采用Cocoapod管理项目依赖,每次拉取最新代码后直接编译可能会报错。这往往是因为其他同事更新了依赖库(新增了第三方库或升级了某些库),而本地还采用之前的第三方库进行编译,从而会出现依赖库缺失或版本不匹配等问题。

应对的做法是,在每次build之前都更新一下Cocoapod。

1
2
3
4
# Update pod repository
pod repo update
# Install pod dependencies
pod install

4、修改编译包的版本号

通过持续集成打包,我们会得到大量的安装包。为了便于区分,比较好的做法是在App中显示版本号,并将版本号与Jenkins的BUILD_NUMBER关联起来。

例如,当前项目的主版本号为2.6.0,本次构建的BUILD_NUMBER为130,那么我们就可以将本次构建的App版本号设置为2.6.0.130。通过这种方式,我们可以通过App中显示的版本号快速定位到具体到构建历史,从而对应到具体的代码提交记录。

要实现对App版本号的设置,只需要在打包前对Info.plist文件中的CFBundleVersion和CFBundleShortVersionString进行修改即可。在Python中,利用plistlib库可以很方便地实现对Info.plist文件的读写。

5、模拟器运行

如果持续集成测试是要运行在iOS模拟器上,那么就需要构建生成.app文件。

在前面讲解的两种构建方式中,中间产物都包含了.app文件。对于以.xcarchive为中间产物的方式,生成的.app文件位于output_dir/StoreCI_Release.xcarchive/Products/Applications/目录中。

不过,这个.app文件在模拟器中还无法直接运行,还需要在Xcode中修改Supported Platforms,例如,将iphoneos更改为iOS。详细原因请参考《从0到1搭建移动App功能自动化测试平台(1):模拟器中运行iOS应用》

关于Android的构建

待续

关于构建脚本

对于构建脚本(build.py)本身,源码应该是最好的说明文档。

在build.py脚本中,主要实现的功能就四点:

  • 执行构建命令,编译生成.ipa文件,这部分包含了关于iOS的构建部分的全部内容;
  • 构建时动态修改Info.plist,将编译包的版本号与Jenkins的BuildNumber关联起来;
  • 上传.ipa文件至pyger/fir.im平台,并且做了失败重试机制;
  • 解析pyger/fir.im平台页面中的二维码,将二维码图片保存到本地。

需要说明的是,对于构建任务中常用的可配置参数,例如BRANCH/SCHEME/CONFIGURATION/OUTPUT_FOLDER等,需要在构建脚本中通过OptionParser的方式实现可传参数机制。这样我们不仅可以命令行中通过传参的方式灵活地调用构建脚本,也可以在Jenkins中实现参数传递。

之所以强调常用的可配置参数,这是为了尽可能减少参数数目,降低脚本调用的复杂度。像PROVISIONING_PROFILE和pgyer/fir.im账号这种比较固定的配置参数,就可以写死在脚本中。因此,在使用构建脚本(build.py)之前,需要先在脚本中配置下PROVISIONING_PROFILE和pgyer/fir.im账号。

另外还想多说一句,pyger/fir.im这类第三方平台在为我们提供便利的同时,稳定性不可控也是一个不得不考虑的问题。在我使用pgyer平台期间,就遇到了平台服务变动、接口时而不稳定出现502等问题。因此,最好的方式还是自行搭建一套类似的服务,反正我是打算这么做了。

Jenkins的详细配置

对于Jenkins的详细配置,需要补充说明的有四点。

1、参数的传递

在构建脚本中,我们已经对常用的可配置参数实现了可传参机制。例如,在Terminal中可以通过如下形式调用构建脚本。

1
$ python build.py --scheme SCHEME --workspace Store.xcworkspace --configuration CONFIGURATION --output OUTPUT_FOLDER

那么我们在Jenkins中要怎样才能指定参数呢?

实际上,Jenkins针对项目具有参数化的功能。在项目的配置选项中,勾选This project is parameterized后,就可以为当前project添加多种类型的参数,包括:

  • Boolean Parameter
  • Choice Parameter
  • Credentials Parameter
  • File Parameter
  • Multi-line String Parameter
  • Password Parameter
  • Run Parameter
  • String Parameter

通常,我们可以选择使用String Parameter来定义自定义参数,并可对每个参数设置默认值。

当我们配置了BRANCH、SCHEME、CONFIGURATION、OUTPUT_FOLDER、BUILD_VERSION这几个参数后,我们就可以在Build配置区域的Execute shell通过如下形式来进行参数传递。

1
2
3
4
5
6
$ python ${WORKSPACE}/Build_scripts/build.py \
--scheme ${SCHEME} \
--workspace ${WORKSPACE}/Store.xcworkspace \
--configuration ${CONFIGURATION} \
--output ${WORKSPACE}/${OUTPUT_FOLDER} \
--build_version ${BUILD_VERSION}.${BUILD_NUMBER}

可以看出,参数的传递方式很简单,只需要预先定义好了自定义参数,然后就可以通过${Param}的形式来进行调用了。

不过你也许会问,WORKSPACE和BUILD_NUMBER这两个参数我们并未进行定义,为什么也能进行调用呢?这是因为Jenkins自带部分与项目相关的环境变量,例如BRANCH_NAME、JOB_NAME等,这部分参数可以在shell脚本中直接进行调用。完整的环境变量可在Jenkins_Url/env-vars.html/中查看。

配置完成后,就可以在Build with Parameters中通过如下形式手动触发构建。

Jenkins manul build

2、修改build名称

在Build History列表中,构建任务的名称默认显示为按照build次数递增的BUILD_NUMBER。有时候我们可能想在build名称中包含更多的信息,例如包含当次构建的SCHEME和CONFIGURATION,这时我们就可以通过修改BuildName实现。

Jenkins默认不支持BuildName设置,但可通过安装build-name-setter插件进行实现。安装build-name-setter插件后,在配置页面的Build Environment栏目下会出现Set Build Name配置项,然后在Build Name中就可以通过环境变量参数来设置build名称。

例如,要将build名称设置为上面截图中的StoreCI_Release_#130样式,就可以在Build Name中配置为${SCHEME}_${CONFIGURATION}_#${BUILD_NUMBER}。

除了在Build Name中传递环境变量参数,build-name-setter还可以实现许多更加强大的自定义功能,大家可自行探索。

3、展示二维码图片

然后再说下如何在Build History列表中展示每次构建对应的二维码图片。

Jenkins build history

需要说明的是,在上图中,绿色框对应的内容是BuildName,我们可以通过build-name-setter插件来实现自定义配置;但是红色框已经不在BuildName的范围之内,而是对应的BuildDescription。

同样地,Jenkins默认不支持在构建过程中自动修改BuildDescription,需要通过安装description setter plugin插件来辅助实现。安装description setter plugin插件后,在配置页面的Build栏目下,Add build step中会出现Set build description配置项,添加该配置项后就会出现如下配置框。

Jenkins set build description

该功能的强大之处在于,它可以在构建日志中通过正则表达式来匹配内容,并将匹配到的内容添加到BuildDescription中去。

例如,我们想要展示的二维码图片是在每次构建过程中生成的,因此我们首先要获取到二维码图片文件。

我的做法是,在build.py中将蒲公英平台返回的应用下载页面地址和二维码图片地址打印到log中。

1
2
3
appDownloadPage: https://www.pgyer.com/035aaf10acf5dd7c279c4fe423a57674
appQRCodeURL: https://o1wjx1evz.qnssl.com/app/qrcodeHistory/fe7a8c9051f0c7fc0affc78f40c20a4b5e4bdb4c77b91a29501f55fd9039c659
Save QRCode image to file: /Users/Leo/.jenkins/workspace/DebugTalk_Plus_Store_iOS/build_outputs/QRCode.png

然后,在Set build description配置项的Regular expression就可以按照如下正则表达式进行匹配:

1
appDownloadPage: (.*)$

接下来,就可以在Description中对匹配到的结果进行引用。

1
<img src='${BUILD_URL}artifact/build_outputs/QRCode.png'>\n<a href='\1'>Install Online</a>

在这里,我们用到了HTML的标签,而Jenkins的Markup Formatter默认是采用Plain text模式,因此还需要对Jenkins对系统配置进行修改,在《使用Jenkins搭建iOS/Android持续集成打包平台》中已进行了详细说明,在此就不再重复。

通过以上方式,就可以实现前面图片中的效果。

4、收集编译成果物

在上面讲解的展示二维码图片一节中,用到了${BUILD_URL}artifact/build_outputs/QRCode.png一项,这里的URL就是用到了编译成果物收集后保存的路径。

Archives build artifacts是Jenkins默认自带的功能,无需安装插件。该功能在配置页面的Post-build Actions栏目下,在Add post-build action的列表中选择添加Archives build artifacts。

添加后的配置页面如下图所示:

Jenkins archive the artifacts

通常,我们只需要配置Files to archive即可。定位文件时,可以通过正则表达式进行匹配,也可以调用项目的环境变量;多个文件通过逗号进行分隔。

例如,假如我们想收集QRCode.png、StoreCI_Release.ipa、Info.plist这三个文件,那么我们就可以通过如下表达式来进行指定。

1
${OUTPUT_FOLDER}/*.ipa,${OUTPUT_FOLDER}/QRCode.png,${OUTPUT_FOLDER}/*.xcarchive/Info.plist

当然,目标文件的具体位置是我们在构建脚本(build.py)中预先进行处理的。

通过这种方式,我们就可以实现在每次完成构建后将需要的文件收集起来进行存档,以便后续在Jenkins的任务页面中进行下载。

show artifacts of Jenkins

也可以直接通过归档文件的URL进行访问。例如,上图中QRCode.png的URL为Jenkins_Url/job/JenkinsJobName/131/artifact/build_outputs/QRCode.png,而Jenkins_Url/job/JenkinsJobName/131/即是${BUILD_URL},因此可以直接通过${BUILD_URL}artifact/build_outputs/QRCode.png引用。

总结

至此,《使用Jenkins搭建iOS/Android持续集成打包平台》一文中涉及到的Jenkins配置和构建脚本实现细节均已补充完毕了。相信大家结合这两篇文章,应该会对如何使用Jenkins搭建iOS/Android持续集成打包平台的基础概念和实现细节都有一个比较清晰的认识。

对于还未完善的部分,我后续将在博客中进行更新。

操作手册请参考文章末尾的【开箱即用】部分,祝大家玩得愉快!

开箱即用

GitHub地址:https://github.com/debugtalk/JenkinsTemplateForApp

1、添加构建脚本

  • 在构建脚本中配置PROVISIONING_PROFILE和pgyer/fir.im账号;
  • 在目标构建代码库的根目录中,创建Build_scripts文件夹,并将build.py拷贝到Build_scripts中;
  • 将Build_scripts/build.py提交到项目中。

除了与Jenkins实现持续集成,构建脚本还可单独使用,使用方式如下:

1
2
3
4
5
$ python ${WORKSPACE}/Build_scripts/build.py \
--scheme ${SCHEME} \
--workspace ${WORKSPACE}/Store.xcworkspace \
--configuration ${CONFIGURATION} \
--output ${WORKSPACE}/${OUTPUT_FOLDER}

2、运行jenkins,安装必备插件

1
$ nohup java -jar jenkins_located_path/jenkins.war &

3、创建Jenkins Job

  • 在Jenkins中创建一个Freestyle project类型的Job,先不进行任何配置;
  • 然后将config.xml文件拷贝到~/.jenkins/jobs/YourProject/中覆盖原有配置文件,重启Jenkins;
  • 完成配置文件替换和重启后,刚创建好的Job就已完成了大部分配置;
  • 在Job Configure中根据项目实际情况调整配置,其中Git Repositories是必须修改的,其它配置项可选择性地进行调整。

4、done!

Introduction to AppiumBooster

发表于 2016-07-20 | 更新于 2019-04-03 | 分类于 Testing

AppiumBooster

AppiumBooster helps you to write automation testcases in yaml format or csv tables, without writing a snippet of code.

write testcases in yaml (recommended)

Take DebugTalk+ Discover’s login and logout function as an example.

preview of login and logout

In order to test these functions above, you can write testcases in yaml format like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ios/testcases/Account.yml
---
AccountTestcases:
login with valid account:
- AccountSteps | enter My Account page
- AccountSteps | enter Login page
- AccountSteps | input EmailAddress
- AccountSteps | input Password
- AccountSteps | login
- AccountSteps | close coupon popup window(optional)

logout:
- AccountSteps | enter My Account page
- SettingsSteps | enter Settings page
- AccountSteps | logout

In the testcases, each step is combined with two parts, joined by a separator |. The former part indicates step file located in ios/steps/ directory, and the latter part indicates testcase step name, which is defined in steps yaml files like below.

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
# ios/steps/AccountSteps.yml
---
AccountSteps:
enter My Account page:
control_id: btnMenuMyAccount
control_action: click
expectation: tablecellMyAccountSystemSettings

enter Login page:
control_id: tablecellMyAccountLogin
control_action: click
expectation: btnForgetPassword

input EmailAddress:
control_id: txtfieldEmailAddress
control_action: type
data: leo.lee@debugtalk.com
expectation: sectxtfieldPassword

input Password:
control_id: sectxtfieldPassword
control_action: type
data: 123456
expectation: btnLogin

login:
control_id: btnLogin
control_action: click
expectation: tablecellMyMessage

write testcases in tables

You can also write testcases in any table tools, including MS Excel and iWork Numbers, and even in plain CSV format.

In order to test the same functions above, you can write testcases in tables like this.

testcases of login and logout

After the testcases are finished, export to CSV format, and put the csv files under ios/testcases/ directory.

run

Once the testcases are done, you are ready to run automation test on your app.

Run the automation testcases is very easy. You can execute ruby run.rb -h in the project root directory to see the usage.

1
2
3
4
5
6
➜  AppiumBooster git:(master) ✗ ruby run.rb -h
Usage: run.rb [options]
-p, --app_path APP_PATH Specify app path
-t, --app_type APP_TYPE Specify app type, ios or android
-f, --testcase_file TESTCASE_FILE Specify testcase file
--disable_output_color Disable output color

AppiumBooster will load all the csv test suites and then excute each suite sequentially.

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
➜  AppiumBooster git:(master) ✗ ruby run.rb -p "ios/app/test.zip" -f "ios/testcases/Account.yml"
initialize appium driver ...
load testcase yaml file: /Users/Leo/MyProjects/AppiumBooster/ios/testcases/Account.yml
load steps yaml file: /Users/Leo/MyProjects/AppiumBooster/ios/steps/AccountSteps.yml
load steps yaml file: /Users/Leo/MyProjects/AppiumBooster/ios/steps/SettingsSteps.yml
start appium driver ...

======= start to run testcase suite: /Users/Leo/MyProjects/AppiumBooster/ios/testcases/Account.yml =======
B------ Start to run testcase: login with valid account
step_1: enter My Account page
btnMenuMyAccount.click ... ✓
step_2: enter Login page
tablecellMyAccountLogin.click ... ✓
step_3: input EmailAddress
txtfieldEmailAddress.type leo.lee@debugtalk.com ... ✓
step_4: input Password
sectxtfieldPassword.type 123456 ... ✓
step_5: login
btnLogin.click ... ✓
step_6: close coupon popup window(optional)
btnClose.click ... ✓
E------ login with valid account

B------ Start to run testcase: logout
step_1: enter My Account page
btnMenuMyAccount.click ... ✓
step_2: enter Settings page
tablecellMyAccountSystemSettings.click ... ✓
step_3: logout
btnLogout.click ... ✓
E------ logout

============ all testcases have been executed. ============
quit appium driver.

Source Code

GitHub: https://github.com/debugtalk/AppiumBooster

使用 Jenkins 搭建 iOS/Android 持续集成打包平台

发表于 2016-06-28 | 更新于 2019-04-03 | 分类于 Development

背景描述

根据项目需求,现要在团队内部搭建一个统一的打包平台,实现对iOS和Android项目的打包。而且为了方便团队内部的测试包分发,希望在打包完成后能生成一个二维码,体验用户(产品、运营、测试等人员)通过手机扫描二维码后就能直接安装测试包。

该需求具有一定的普遍性,基本上所有开发APP的团队都可能会用到,因此我将整个需求实现的过程整理后形成此文,并且真正地做到了零基础上手,到手即飞、开箱即用,希望能对大家有所帮助。

首先,先给大家展示下平台建设完成后的整体效果:

Overview of Jenkins Job
Build view of Jenkins Job

该平台主要实现的功能有3点:

  • 定期对GitHub仓库进行检测,若有更新则自动执行构建打包;
  • 构建成功后根据ipa/apk生成二维码,并可在历史构建列表中展示各个版本的二维码,通过手机扫描二维码可直接安装对应版本;
  • 在构建结果页面中展示当次构建的成果物(Artifact,如.ipa、.app、.apk、info.plist等文件),供有需要的用户进行下载。

接下来,本文就开始对平台建设的完整实现过程进行详细介绍。

安装Jenkins

Jenkins依赖于Java运行环境,因此需要首先安装Java。

安装Jenkins的方式有多种,可以运行对应系统类型的安装包,可以通过docker获取镜像,也可以直接运行war包。

我个人倾向于直接运行war包的形式,只需下载jenkins.war后,运行如下命令即可启动Jenkins。

1
$ nohup java -jar jenkins_located_path/jenkins.war --httpPort=88 &

如果不指定httpPort,Jenkins的默认端口为8080。

Jenkins插件

Jenkins有非常多的插件,可以实现各种功能的扩展。

针对搭建的iOS/Android持续集成打包平台,我使用到了如下几个插件。

  • GIT plugin
  • SSH Credentials Plugin
  • Git Changelog Plugin: 获取仓库提交的commit log
  • build-name-setter:用于修改Build名称
  • description setter plugin:用于在修改Build描述信息,在描述信息中增加显示QRCode(二维码)
  • Post-Build Script Plug-in:在编译完成后通过执行脚本实现一些额外功能
  • Xcode integration: iOS专用(可选)
  • Gradle plugin: Android专用(可选)

安装方式也比较简单,直接在Jenkins的插件管理页面搜索上述插件,点击安装即可。

创建项目(Job)

在Jenkins中,构建项目以Job的形式存在,因此需要针对每个项目创建一个Job。有时候,一个项目中可能有多个分支同时在进行开发,为了分别进行构建,也可以针对每个分支创建一个Job。

创建Job的方式有多种,本次只需要创建Freestyle project类型的即可。

Main page -> New Item -> Freestyle project

对于一个持续集成打包平台,每次打包都由4步组成:触发构建、拉取代码、执行构建、构建后处理。对应的,在每个Job中也对应了这几项的配置。

配置Git代码仓库

要对项目进行构建,配置项目的代码仓库是必不可少的。由于当前我们的项目托管在GitHub私有仓库中,因此在此需要对Git进行配置。

在【Source Code Management】配置栏目下,如果之前GIT plugin安装成功,则会出现Git选项。

配置Git代码仓库时,有三项是必须配置的:仓库URL地址(Repository URL)、仓库权限校验方式(Credentials),以及当前Job需要构建的代码分支(Branches to build)。

在配置Repository URL时,选择HTTPS URL或SSH URL均可。不过需要注意的是,Credentials要和Repository URL对应,也就是说:

  • 如果Repository URL是HTTPS URL形式的,那么Credentials就要采用GitHub用户名密码的校验方式;而且,如果在GitHub中开启了2FA(two-factor authentication),那么还需要在GitHub中创建一个Personal access token,输入密码时将这个Personal access token作为密码进行输入。
  • 如果Repository URL是SSH URL形式的,那么就需要先在Jenkins所在的服务器上创建一个SSH秘钥对,并将公钥添加到GitHub的SSH keys中,然后在填写Credentials时,选择SSH Username with private key的校验方式,填入GitHub Username、SSH私钥、以及创建SSH秘钥对时设置的Passphrase。

如果对Git权限校验的概念还比较模糊,可以参考《深入浅出Git权限校验》。

在配置Branches to build时,可以采用多种形式,包括分支名称(branchName)、tagName、commitId等。其中分支名称的形式用的最多,例如,若是构建master分支,则填写refs/heads/master,若是构建develop分支,则填写refs/heads/develop。

除了以上关于Git的必填配置项,有时根据项目的实际情况,可能还需要对Jenkins的默认配置项进行修改。

比较常见的一种情况就是对clone的配置进行修改。

在Jenkins的默认配置中,clone代码时会拉取所有历史版本的代码,而且默认的超时时限只有10分钟。这就造成在某些项目中,由于代码量本身就比较大,历史版本也比较多,再加上网络环境不是特别好,Jenkins根本没法在10分钟之内拉取完所有代码,超时后任务就会被自动终止了(错误状态码143)。

这种问题的解决方式也很简单,无非就是两种思路,要么少拉取点代码(不获取历史版本),要么提高超时时限。对应的配置在Advanced clone behaviours中:

  • Shallow clone:勾选后不获取历史版本;
  • Timeout (in minutes) for clone and fetch operation:配置后覆盖默认的超时时限。

配置构建触发器

代码仓库配置好了,意味着Jenkins具有了访问GitHub代码仓库的权限,可以成功地拉取代码。

那Jenkins什么时候执行构建呢?

这就需要配置构建触发策略,即构建触发器,配置项位于【Build Triggers】栏目。

触发器支持多种类型,常用的有:

  • 定期进行构建(Build periodically)
  • 根据提交进行构建(Build when a change is pushed to GitHub)
  • 定期检测代码更新,如有更新则进行构建(Poll SCM)

构建触发器的选择为复合选项,若选择多种类型,则任一类型满足构建条件时就会执行构建工作。如果所有类型都不选择,则该Jenkins Job不执行自动构建,但可通过手动点击【Build Now】触发构建。

关于定时器(Schedule)的格式,简述如下:

MINUTE HOUR DOM MONTH DOW

  • MINUTE: Minutes within the hour (0-59)
  • HOUR: The hour of the day (0-23)
  • DOM: The day of the month (1-31)
  • MONTH: The month (1-12)
  • DOW: The day of the week (0-7) where 0 and 7 are Sunday.

通常情况下需要指定多个值,这时可以采用如下operator(优先级从上到下):

  • *适配所有有效的值,若不指定某一项,则以*占位;
  • M-N适配值域范围,例如7-9代表7/8/9均满足;
  • M-N/X或*/X:以X作为间隔;
  • A,B,C:枚举多个值。

另外,为了避免多个任务在同一时刻同时触发构建,在指定时间段时可以配合使用H字符。添加H字符后,Jenkins会在指定时间段内随机选择一个时间点作为起始时刻,然后加上设定的时间间隔,计算得到后续的时间点。直到下一个周期时,Jenkins又会重新随机选择一个时间点作为起始时刻,依次类推。

为了便于理解,列举几个示例:

  • H/15 * * * *:代表每隔15分钟,并且开始时间不确定,这个小时可能是:07,:22,:37,:52,下一个小时就可能是:03,:18,:33,:48;
  • H(0-29)/10 * * * *:代表前半小时内每隔10分钟,并且开始时间不确定,这个小时可能是:04,:14,:24,下一个小时就可能是:09,:19,:29;
  • H 23 * * 1-5:工作日每晚23:00至23:59之间的某一时刻;

配置构建方式

触发策略配置好之后,Jenkins就会按照设定的策略自动执行构建。但如何执行构建操作,这还需要我们通过配置构建方式来进行设定。

常用的构建方式是根据构建对象的具体类型,安装对应的插件,然后采用相应的构建方式。例如,若是构建Android应用,安装Gradle plugin之后,就可以选择Invoke Gradle script,然后采用Gradle进行构建;若是构建iOS应用,安装Xcode integration插件之后,就可以选择Xcode,然后选择Xcode进行构建。

该种方式的优势是操作简单,UI可视化,在场景不复杂的情况下可以快速满足需求。不过缺点就是依赖于插件已有的功能,如果场景较复杂时可能单个插件还无法满足需求,需要再安装其它插件。而且,有些插件可能还存在一些问题,例如对某些操作系统版本或XCode版本兼容不佳,出现问题时我们就会比较被动。

我个人更倾向于另外一种方式,就是自己编写打包脚本,在脚本中自定义实现所有的构建功能,然后在Execute Shell中执行。这种方式的灵活度更高,各种场景的构建需求都能满足,出现问题后也能自行快速修复。

另外,对于iOS应用的构建,还有一个需要额外关注的点,就是开发者证书的配置。

如果是采用Xcode integration插件进行构建,配置会比较复杂,需要在Jenkins中导入开发证书,并填写多个配置项。不过,如果是采用打包脚本进行构建的话,情况就会简单许多。只要在Jenkins所运行的计算机中安装好开发者证书,打包命令在Shell中能正常工作,那么在Jenkins中执行打包脚本也不会有什么问题。

构建后处理

完成构建后,生成的编译成果物(ipa/apk)会位于指定的目录中。但是,如果要直接在手机中安装ipa/apk文件还比较麻烦,不仅在分发测试包时需要将好几十兆的安装包进行传送,体验用户在安装时也还需要通过数据线将手机与计算机进行连接,然后再使用PP助手或豌豆荚等工具进行安装。

当前比较优雅的一种方式是借助蒲公英(pgyer)或fir.im等平台,将ipa/apk文件上传至平台后由平台生成二维码,然后只需要对二维码链接进行分发,体验用户通过手机扫描二维码后即可实现快速安装,效率得到了极大的提升。

上传安装包文件,生成二维码

不管是蒲公英还是fir.im,都有对应的Jenkins插件,安装插件后可以在Post-build中实现对安装包的上传。

除了使用Jenkins插件,fir.im还支持命令上传的方式,蒲公英还支持HTTP Post接口上传的方式。

我个人推荐采用命令或接口上传的方法,并在构建脚本中进行调用。灵活是一方面,更大的好处是如果上传失败后还能进行重试,这在网络环境不是很稳定的情况下极其必要。

Jenkins成功完成安装包上传后,pgyer/fir.im平台会生成一个二维码图片,并在响应中将图片的URL链接地址进行返回。

展示二维码图片

二维码图片的URL链接有了,那要怎样才能将二维码图片展示在Jenkins项目的历史构建列表中呢?

这里需要用到另外一个插件,description setter plugin。安装该插件后,在【Post-build Actions】栏目中会多出description setter功能,可以实现构建完成后设置当次build的描述信息。这个描述信息不仅会显示在build页面中,同时也会显示在历史构建列表中。

有了这个前提,要将二维码图片展示在历史构建列表中貌似就可以实现了,能直观想到的方式就是采用HTML的img标签,将<img src='qr_code_url'>写入到build描述信息中。

这个方法的思路是正确的,不过这么做以后并不会实现我们预期的效果。

这是因为Jenkins出于安全的考虑,所有描述信息的Markup Formatter默认都是采用Plain text模式,在这种模式下是不会对build描述信息中的HTML编码进行解析的。

要改变也很容易,Manage Jenkins -> Configure Global Security,将Markup Formatter的设置更改为Safe HTML即可。

更改配置后,我们就可以在build描述信息中采用HTML的img标签插入图片了。

另外还需要补充一个点。如果是使用蒲公英(pyger)平台,会发现每次上传安装包后返回的二维码图片是一个短链接,神奇的是这个短连接居然是固定的(对同一个账号而言)。这个短连接总是指向最近生成的二维码图片,但是对于二维码图片的唯一URL地址,平台并没有在响应中进行返回。在这种情况下,我们每次构建完成后保存二维码图片的URL链接就没有意义了。

应对的做法是,每次上传完安装包后,通过返回的二维码图片短链接将二维码图片下载并保存到本地,然后在build描述信息中引用该图片在Jenkins中的地址即可。

收集编译成果物(Artifacts)

每次完成构建后,编译生成的文件较多,但是并不是所有的文件都是我们需要的。

通常情况下,我们可能只需要其中的部分文件,例如.ipa/.app/.plist/.apk等,这时我们可以将这部分文件单独收集起来,并在构建页面中展示出来,以便在需要时进行下载。

要实现这样一个功能,需要在【Post-build Actions】栏目中新增Archive the artifacts,然后在Files to archive中通过正则表达式指定成果物文件的路径。

设置完毕后,每次构建完成后,Jenkins会在Console Output中采用设定的正则表达式进行搜索匹配,如果能成功匹配到文件,则会将文件收集起来。

总结

本文主要是对如何使用Jenkins搭建iOS/Android持续集成打包平台的基础概念和实施流程进行了介绍。对于其中涉及到的执行命令、构建脚本(build.py),以及Jenkins的详细配置,出于篇幅长度和阅读体验的考虑,并没有在文中进行详细展开。

为了实现真正的开箱即用,我将Jenkins的配置文件和构建脚本抽离出来形成一套模板,只需要导入到Jenkins中,然后针对具体的项目修改少量配置信息,即可将这一套持续集成打包平台运行起来,实现和文章开头插图中完全相同的功能效果。

详细内容请阅读《关于持续集成打包平台的Jenkins配置和构建脚本实现细节》。

深入浅出 Git 权限校验

发表于 2016-06-15 | 更新于 2019-04-03 | 分类于 Development

借助上次“掉坑”的经历,我对Git权限校验的两种方式重头进行了梳理,形成了这篇总结记录。

在本地计算机与GitHub(或GitLab)进行通信时,传输主要基于两种协议,HTTPS和SSH,对应的仓库地址就是HTTPS URLs和SSH URLs。

首先需要强调的是,HTTPS URLs和SSH URLs对应的是两套完全独立的权限校验方式,主要的区别就是HTTPS URLs采用账号密码进行校验,SSH URLs采用SSH秘钥对进行校验。平时使用的时候我们可以根据实际情况,选择一种即可。

HTTPS URLs

GitHub官方推荐采用HTTPS URLs的方式,因为该种方式适用面更广(即使在有防火墙或代理的情况下也同样适用),使用更方便(配置更简单)。

采用HTTPS URLs地址clone/fetch/pull/push仓库时,事先无需对本地系统进行任何配置,只需要输入GitHub的账号和密码即可。不过如果每次都要手动输入账号密码,也是一件很繁琐的事情。

好在已经有多个机制可以让操作不用这么麻烦。

在Mac系统中,在启用Keychain机制的情况下,首次输入GitHub账号密码后,认证信息就会自动保存到系统的Keychain中,下次再次访问仓库时就会自动读取Keychain中保存的认证信息。

在非Mac系统中,虽然没有Keychain机制,但是Git提供了credential helper机制,可以将账号密码以cache的形式在内存中缓存一段时间(默认15分钟),或者以文件的形式存储起来(~/.git-credentials)。当然,Mac系统如果不启用Keychain机制,也可以采用这种方式。

1
2
3
4
# cache credential in memory
$ git config --global credential.helper cache
# store credential in ~/.git-credential
$ git config --global credential.helper store

在credential.helper设置为store的情况下,首次输入GitHub账号密码后,就会自动保存到~/.git-credentials文件中,保存形式为https://user:pass@github.com;下次再次访问仓库时就会自动读取~/.git-credentials中保存的认证信息。

另一个需要说明的情况是,如果在GitHub中开启了2FA(two-factor authentication),那么在本地系统中输入GitHub账号密码时,不能输入原始的密码(即GitHub网站的登录密码),而是需要事先在GitHub网站中创建一个Personal access token,后续在访问代码仓库需要进行权限校验的时候,采用access token作为密码进行输入。

SSH URLs

除了HTTPS URLs,还可以采用SSH URLs的方式访问GitHub代码仓库。

采用SSH URLs方式之前,需要先在本地计算机中生成SSH keypair(秘钥对,包括私钥和公钥)。默认情况下,生成的秘钥位于$HOME/.ssh/目录中,文件名称分别为id_rsa和id_rsa.pub,通常无需修改,保持默认即可。不过,如果一台计算机中存在多个秘钥对,就需要修改秘钥文件名,名称没有强制的命名规范,便于自己辨识即可。

如下是创建秘钥对的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
➜ ssh-keygen -t rsa -b 4096 -C "mail@debugtalk.com"
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/Leo/.ssh/id_rsa): /Users/Leo/.ssh/debugtalk_id_rsa
Enter passphrase (empty for no passphrase): <myPassphrase>
Enter same passphrase again: <myPassphrase>
Your identification has been saved in /Users/Leo/.ssh/debugtalk_id_rsa.
Your public key has been saved in /Users/Leo/.ssh/debugtalk_id_rsa.pub.
The key fingerprint is:
SHA256:jCyEEKjlCU1klROnuBg+UH08GJ1u252rQMADdD9kYMo mail@debugtalk.com
The key's randomart image is:
+---[RSA 4096]----+
|+*BoBO+. |
|o=oO=** |
|++E.*+o. |
|+ooo +o+ |
|.o. ..+oS. . |
| . o. . o |
| . . |
| . . |
| .. |
+----[SHA256]-----+

在创建秘钥的过程中,系统还建议创建一个名为passphrase的东西,这是用来干嘛的呢?

首先,单独采用密码肯定是不够安全的。如果密码太简单,那么就很容易被暴力破解,如果密码太复杂,那么用户就很难记忆,记录到小本子里面更不安全。

因此,SSH keys诞生了。SSH秘钥对的可靠性非常高,被暴力破解的可能性基本没有。不过,这要求用户非常谨慎地保管好私钥,如果别人使用你的计算机时偷偷地将你的私钥拷走了,那么就好比是别人拿到了你家里的钥匙,也能随时打开你家的门。

基于以上情况,解决办法就是在SSH keys之外再增加一个密码,即passphrase。只有同时具备SSH private key和passphrase的情况下,才能通过SSH的权限校验,这就大大地增加了安全性。当然,这个passphrase也不是必须的,在创建秘钥对时也可以不设置passphrase。

另外,如果每次权限校验时都要输入passphrase,这也是挺麻烦的。好在我们不用再担心这个问题,因为ssh-agent可以帮我们记住passphrase,Mac系统的Keychain也可以记住passphrase,这样我们在同一台计算机中就不用重新输入密码了。

秘钥对创建好以后,私钥存放于本地计算机(~/.ssh/id_rsa),将公钥(~/.ssh/id_rsa.pub)中的内容添加至GitHub账户。

1
2
3
4
5
# copy the contents of id_rsa.pub to the clipboard
➜ pbcopy < ~/.ssh/id_rsa.pub

# paste to GitHub
# Login GitHub, 【Settings】->【SSH and GPG keys】->【New SSH Key】

不过,如果此时检测本地计算机与GitHub的连接状态,会发现系统仍提示权限校验失败。

1
2
➜ ssh -T git@github.com
Permission denied (publickey).

这是因为在本地计算机与GitHub建立连接的时候,实际上是本机计算机的ssh-agent与GitHub服务器进行通信。虽然本地计算机有了私钥,但是ssh-agent并不知道私钥存储在哪儿。因此,要想正常使用秘钥对,需要先将私钥加入到本地计算机的ssh-agent中(添加过程中需要输入passphrase)。

1
2
3
4
5
6
7
# start ssh-agent in the background
➜ eval "$(ssh-agent -s)"
Agent pid 78370

➜ ssh-add ~/.ssh/id_rsa
Enter passphrase for /Users/Leo/.ssh/id_rsa: <myPassphrase>
Identity added: /Users/Leo/.ssh/id_rsa (/Users/Leo/.ssh/id_rsa)

添加完成后,就可以查看到当前计算机中存储的密钥。

1
2
➜ ssh-add -l
4096 SHA256:xRg49AgTxxxxxxxx8q2SPPOfxxxxxxxxRlBY /Users/Leo/.ssh/id_rsa (RSA)

再次检测本地计算机与GitHub的连接状态,校验就正常通过了。

1
2
➜ ssh -T git@github.com
Hi leolee! You've successfully authenticated, but GitHub does not provide shell access.

后续再进行clone/fetch/pull/push操作时,就可以正常访问GitHub代码仓库了,并且也不需要再重新输入账号密码。

而且,将私钥加入ssh-agent后,即使删除私钥文件,本地计算机仍可以正常访问GitHub代码仓库。

1
2
3
4
5
6
7
8
9
➜ rm -rf ~/.ssh
➜ ssh-add -l
4096 SHA256:xRg49AgTxxxxxxxx8q2SPPOfxxxxxxxxRlBY /Users/Leo/.ssh/id_rsa (RSA)
➜ ssh -T git@github.com
The authenticity of host 'github.com (192.30.252.130)' can't be established.
RSA key fingerprint is SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'github.com,192.30.252.130' (RSA) to the list of known hosts.
Hi leolee! You've successfully authenticated, but GitHub does not provide shell access.

只有执行ssh-add -D或ssh-add -d pub_key命令,将私钥从ssh-agent删除后,认证信息才会失效。

1
2
3
4
5
6
➜ ssh-add -d ~/.ssh/id_rsa.pub
Identity removed: /Users/Leo/.ssh/id_rsa.pub (mail@debugtalk.com)
➜ ssh-add -l
The agent has no identities.
➜ ssh -T git@github.com
Permission denied (publickey).

同时使用多个GitHub账号

熟悉了HTTPS URLs和SSH URLs这两种校验方式之后,我们再来看之前遇到的问题。要想在一台计算机上同时使用多个GitHub账号访问不同的仓库,需要怎么做呢?

为了更好地演示,现假设有两个GitHub账号,debugtalk和leolee,在两个账号中各自有一个仓库,debugtalk/DroidMeter和DebugTalk/MobileStore(公司私有库)。

前面已经说过,HTTPS URLs和SSH URLs对应着两套独立的权限校验方式,因此这两套方式应该是都能单独实现我们的需求的。

不过在详细讲解Git权限校验的问题之前,我们先来回顾下Git配置文件的优先级。

Git配置存储位置及其优先级

Unix-like系统中,保存Git用户信息的主要有3个地方(Mac系统多一个Keychain):

  • /etc/gitconfig:存储当前系统所有用户的git配置信息,使用带有--system选项的git config时,配置信息会写入该文件;
  • ~/.gitconfig或~/.config/git/config:存储当前用户的git配置信息,使用带有--global选项的git config时,配置信息会写入该文件;
  • Keychain Access:在开启Keychain机制的情况下,进行权限校验后会自动将账号密码保存至Keychain Access。
  • 仓库的Git目录中的config文件(即repo/.git/config):存储当前仓库的git配置信息,在仓库中使用带有--local选项的git config时,配置信息会写入该文件;

在优先级方面,以上4个配置项的优先级从上往下依次上升,即repo/.git/config的优先级最高,然后Keychain Access会覆盖~/.gitconfig中的配置,~/.gitconfig会覆盖/etc/gitconfig中的配置。

基于SSH协议实现多账号共存

先来看下如何采用SSH URLs实现我们的需求。

在处理多账号共存问题之前,两个账号均已分别创建SSH秘钥对,并且SSH-key均已加入本地计算机的ssh-agent。

1
2
3
➜ ssh-add -l
4096 SHA256:lqujbjkWM1xxxxxxxxxxG6ERK6DNYj9tXExxxxxx8ew /Users/Leo/.ssh/debugtalk_id_rsa (RSA)
4096 SHA256:II2O9vZutdQr8xxxxxxxxxxD7EYvxxxxxxbynx2hHtg /Users/Leo/.ssh/id_rsa (RSA)

在详细讲解多账号共存的问题之前,我们先来回想下平时在Terminal中与GitHub仓库进行交互的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
➜  DroidMeter git:(master) git pull
Already up-to-date.
➜ DroidMeter git:(master) touch README.md
➜ DroidMeter git:(master) ✗ git add .
➜ DroidMeter git:(master) ✗ git commit -m "add README"
➜ DroidMeter git:(master) git push
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 310 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To git@debugtalk:debugtalk/DroidMeter.git
7df6839..68d085b master -> master

在操作过程中,本地计算机的ssh-agent与GitHub服务器建立了连接,并进行了账号权限校验。

当本地计算机只有一个GitHub账号时,这个行为并不难理解,系统应该会采用这个唯一的GitHub账号进行操作。那如果本地计算机中有多个Github账号时,系统是根据什么来判断应该选择哪个账号呢?

实际情况是,系统没法进行判断。系统只会有一个默认的账号,然后采用这个默认的账号去操作所有的代码仓库,当账号与仓库不匹配时,就会报权限校验失败的错误。

那要怎样才能让系统正确区分账号呢?这就需要我们手动进行配置,配置文件即是~/.ssh/config。

创建~/.ssh/config文件,在其中填写如下内容。

1
2
3
4
5
6
7
8
9
10
11
# debugtalk
Host debugtalk
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa

# DT
Host leolee
HostName github.com
User git
IdentityFile ~/.ssh/dt_id_rsa

要理解以上配置文件的含义并不难,我们可以对比看下两个项目的SSH URLs:

1
2
git@github.com:debugtalk/DroidMeter.git
git@github.com:DTSZ/Store_Android.git

其中,git是本地ssh-agent与GitHub服务器建立SSH连接采用的用户名(即User),github.com是GitHub服务器的主机(即HostName)。

可以看出,如果采用原始的SSH URLs,由于User和HostName都相同,本地计算机并不知道应该采用哪个SSH-key去建立连接。

因此,通过创建~/.ssh/config文件,在Host中进行区分,然后经过CNAME映射到HostName,然后分别指向不同的SSH-key,即IdentityFile。由于HostName才是真正指定GitHub服务器主机的字段,因此这么配置不会对本地ssh-agent连接GitHub主机产生影响,再加上Host别名指向了不同的SSH-key,从而实现了对两个GitHub账号的分离。

配置完毕后,两个GitHub账号就可以通过Host别名来进行区分了。后续再与GitHub服务器进行通信时,就可以采用Host别名代替原先的github.com。例如,测试本地ssh-agent与GitHub服务器的连通性时,可采用如下方式:

1
2
3
4
➜ ssh -T git@debugtalk
Hi debugtalk! You have successfully authenticated, but GitHub does not provide shell access.
➜ ssh -T git@leolee
Hi leolee! You have successfully authenticated, but GitHub does not provide shell access.

可以看出,此时两个账号各司其职,不会再出现混淆的情况。

不过,我们还遗漏了很重要的一点。在本地代码仓库中执行push/pull/fetch等操作的时候,命令中并不会包含Host信息,那系统怎么知道我们要采用哪个GitHub账号进行操作呢?

答案是,系统还是没法判断,需要我们进行配置指定。

显然,不同的仓库可能对应着不同的GitHub账号,因此这个配置不能配置成全局的,而只能在各个项目中分别进行配置,即repo/.git/config文件。

配置的方式如下:

在debugtalk/DroidMeter仓库中:

1
➜ git remote add origin git@debugtalk:debugtalk/DroidMeter.git

在DebugTalk/MobileStore.git仓库中:

1
➜ git remote add origin git@leolee:DebugTalk/MobileStore.git

配置的原理也很容易理解,就是将仓库的Host更换为之前设置的别名。添加完毕后,后续再在两个仓库中执行任何git操作时,系统就可以选择正确的SSH-key与GitHub服务器进行交互了。

基于HTTPS协议实现多账号共存

再来看下如何采用HTTPS URLs实现我们的需求。

有了前面的经验,我们的思路就清晰了许多。采用HTTPS URLs的方式进行Git权限校验后,系统会将GitHub账号密码存储到Keychain中(Mac系统),或者存储到~/.git-credentials文件中(Git credential helper)。

不管是存储到哪里,我们面临的问题都是相同的,即如何在代码仓库中区分采用哪个GitHub账号。

配置的方式其实也很简单:

在debugtalk/DroidMeter仓库中:

1
➜ git remote add origin https://debugtalk@github.com/debugtalk/DroidMeter.git

在DebugTalk/MobileStore.git仓库中:

1
➜ git remote add origin https://leolee@github.com/DebugTalk/MobileStore.git

配置的原理也很容易理解,将GitHub用户名添加到仓库的Git地址中,这样在执行git命令的时候,系统就会采用指定的GitHub用户名去Keychain或~/.git-credentials中寻找对应的认证信息,账号使用错乱的问题也就不复存在了。

Done!

GitHub 权限校验失败给我的启发

发表于 2016-06-14 | 更新于 2019-04-03 | 分类于 Development

背景描述

众所周知,在GitHub中,每个仓库都有两个地址,分别基于HTTPS协议和SSH协议,两个协议对应的URL地址(repository_url)形式如下所示:

1
2
3
4
# HTTPS
https://github.com/XY/MobileStore.git
# SSH
git@github.com:XY/MobileStore.git

正常情况下,只要在本地正确地配置好了git账号,采用这两个地址中的任意一个,都可以通过git clone repository_url获取代码。

但最近我在Macbook Air中clone公司托管在GitHub私有库中的代码时,发现无法通过HTTPS协议的地址clone代码,始终提示remote: Repository not found.的错误。

1
2
3
4
➜ git clone https://github.com/XY/MobileStore.git
Cloning into 'MobileStore'...
remote: Repository not found.
fatal: repository 'https://github.com/XY/MobileStore.git/' not found

首先,这个代码仓库是确实存在的,而且地址肯定也是没有问题的,通过URL地址也能在浏览器中访问到对应的GitHub仓库页面。

其次,在本地对git的配置也是没有问题的,通过SSH协议的地址是可以正常clone代码的。

1
2
3
4
5
➜ git clone git@github.com:XY/MobileStore.git
Cloning into 'MobileStore'...
Warning: Permanently added the RSA host key for IP address '192.30.252.131' to the list of known hosts.
remote: Counting objects: 355, done.
remote: Compressing objects: 100% (3/3), done.

并且,如果在HTTPS协议的URL地址中加上GitHub账号,也是可以正常clone代码的。

1
2
3
4
➜ git clone https://leolee@github.com/XY/MobileStore.git
Cloning into 'MobileStore'...
remote: Counting objects: 355, done.
remote: Compressing objects: 100% (3/3), done.

更奇怪的是,在我的另一台Mac Mini中,采用同样的账号配置,两种协议的URL地址却都能正常clone代码,仔细地对比了两台电脑的git配置,都是一样的。

1
2
3
4
5
6
➜ cat ~/.git-credentials
https://leolee:340d247cxxxxxxxxf39556e38fe2b0baxxxxxxxx@github.com
➜
➜ cat ~/.gitconfig
[credential]
helper = store

那问题出在哪儿呢?

定位分析

通过Google得知,产生remote: Repository not found.报错的原因主要有两个,一是仓库地址错误,二是权限校验不通过。显然,第一个原因可以直接排除,在Macbook Air中出现该问题应该就是账号权限校验失败造成的。

对背景描述中的现象进行整理,重点关注两个疑点:

  • 通过HTTPS协议的URL地址进行git clone时,系统没有提示让输入用户名密码,就直接返回权限校验失败的异常;
  • 在HTTPS协议的URL地址中加上GitHub用户名,就可以正常clone,而且,系统也没有提示输入密码。

这说明,在系统中的某个地方,应该是保存了GitHub账号密码的,所以在未指定账号的情况下,git clone时系统就不再要求用户输入账号密码,而是直接读取那个保存好的账号信息;但是,那个保存的GitHub账号密码应该是存在问题的,这就造成采用那个账号信息去GitHub校验时无法通过,从而返回异常报错。

基于以上推测,寻找问题根源的当务之急是找到保存GitHub账号密码的地方。

通过查看Git官方文档,存储Git用户信息的地方有三个:

  • /etc/gitconfig:存储当前系统所有用户的git配置信息;
  • ~/.gitconfig或~/.config/git/config:存储当前用户的git配置信息;
  • 仓库的Git目录中的config文件(即repo/.git/config):存储当前仓库的git配置信息。

这三个配置项的优先级从上往下依次上升,即repo/.git/config会覆盖~/.gitconfig中的配置,~/.gitconfig会覆盖/etc/gitconfig中的配置。

回到当前问题,由于还没有进入到具体的Git仓库,因此repo/.git/config可直接排除;然后是查看当前用户的git配置,在当前用户HOME目录下没有~/.config/git/config文件,只有~/.gitconfig,不过在~/.gitconfig中并没有账号信息;再去查看系统级的git配置信息,即/etc/gitconfig文件,但发现当前系统中并没有该文件。

找遍了Git用户信息可能存储的地方,都没有看到账号配置信息,那还可能存储在哪儿呢?

这时基本上是毫无思路了,只能靠各种胡乱猜测,甚至尝试采用Wireshark分别在两台Mac上对git clone的过程进行抓包,对比通讯数据的差异,但都没有找到答案。

最后,无意中想到了Mac的Keychain机制。在Mac OSX的Keychain中,可以保存用户的账号密码等credentials,那git账号会不会也保存到Keychain中了呢?

在Macbook Air中打开Keychain Access应用软件,搜索github,果然发现存在记录。

Mac Keychain of GitHub

而且,github.com这一项还存在两条记录。一条是我的个人账号debugtalk,另一条是公司的工作账号leolee。

至此,真相大白!!!

在我的Macbook Air中,Keychain Access中保存了我的GitHub个人账号(debugtalk),该账号是没有权限访问公司私有仓库的。但是在Terminal中执行git clone命令时,系统优先读取了我的个人账号,并用该账号向GitHub发起校验请求,从而造成读取公司私有仓库时权限校验失败。然而,在HTTPS协议的URL地址中加上GitHub工作账号(leolee)时,由于此时指定了账号名称,因此在Keychain中读取账号信息时就可以找到对应账号(包含密码),并且在无需输入密码的情况下就能成功通过GitHub的权限校验,进而成功clone得到代码。

原因弄清楚之后,解决方式就很简单了,在Keychain中删除个人账号,然后就正常了。

总结回顾

但是,问题真的解决了么?

并没有!

简单粗暴地在Keychain中将个人GitHub账号删除了,虽然再次访问公司代码仓库时正常了,那我要再访问个人仓库时该怎么办呢?

貌似并没有清晰的思路。虽然网上也有不少操作指导教程,但是对于操作背后的原理,还是有很多不清晰的地方。

再回到前面的背景描述,以及定位问题的整个过程,不由地悲从中来。使用GitHub好歹也有好几年了,但是连最基本的概念都还一头雾水,所以遇到问题后只能靠瞎猜,东碰西撞,最后瞎猫碰到死耗子。

GitHub的HTTPS协议和SSH协议,这本来就对应着两套完全独立的权限校验方式,而我在HTTPS协议不正常的情况下还去查看SSH协议,这本来就实属多余。

借助这次“掉坑”的经历,我对Git权限校验的两种方式重头进行了梳理,并单独写了一篇博客,《深入浅出Git权限校验》,虽然花了些时间,但总算是扫清了萦绕多年的迷雾,感觉倍儿爽!

如果你也对Git的权限校验没有清晰的了解,遇到权限校验出错时只能“换一种方法试试”,也不知道怎么让一台计算机同时支持多个GitHub账号,那么也推荐看下那篇博客。

在微信公众号debugtalk中输入Git权限校验,获取《深入浅出Git权限校验》。

关于促销活动页面测试的那些事儿

发表于 2016-06-09 | 更新于 2019-04-03 | 分类于 Testing , 功能测试

这篇文章来说说促销活动页面测试的那些事儿。

什么是促销活动页面?

通常电商平台在节假日会做一些促销活动,而活动的宣传方式,主要会采用H5静态页面的形式,也就是本文中要讲的促销活动页面。

这些活动页面的特点是元素构成很简单,在页面中只包含一些促销商品的图片及其价格,而且价格往往都是写死在页面中,不会涉及到从数据库中读取,完完全全的静态页面;当然,促销活动页面的目的是将用户流量导向电商平台,因此在页面的图片或购买按钮背后会配上URL链接,用户点击链接后会跳转至电商平台对应的购买页面,活动页面也就完成了使命,这往往就是活动页面的全部内容。

这么看来,促销活动页面跟街上发的传单非常相似,只是传单是纸质的,而活动页面是网页的罢了。

促销活动页面为什么要这么做呢?

四方面原因。

第一,促销活动页面的目的性很强,就是为了主推几款特价商品,因此,商品和价格完全写死也不会有任何问题。

第二,促销活动页面的时效性很强,为促销活动而生,生命周期就那么几天,促销活动结束后这个活动页面也就作废了,因此也不用考虑复用的问题。

第三,从投入的人力成本和工作量考虑,促销活动页面的制作只需要设计师和前端工程师就能完成,无需后台开发人员进行配合,也无需对当前的电商平台进行任何功能调整。

第四,电商平台在做促销活动时,往往会通过各种渠道进行推广,因此活动页面的访问流量是非常巨大的;而采用静态页面的形式,不仅可以极大地提高页面加载速度(图片等静态资源可以通过CDN存储),而且可以极大地减轻流量对电商平台服务器端的压力(用户在浏览活动页面的时候并不会与电商平台进行任何交互)。至于点击链接进入电商平台的流量嘛,毕竟转换率总是存在的,转换以后的流量会小很多,而这部分流量才是真正有效的。

如何对活动页面进行测试

那么,就这么简单的一个静态页面,还需要对它进行测试么?

咋一看,貌似还真没有可测试的内容,因为页面中就找不到一个真正意义上的功能点。

然而,历史经验表明,在软件工程中无论多简单的功能,都是有可能出现bug的。针对活动页面的形式,我们需要重点关注如下几点。

1、商品信息一致性

促销活动页面中的商品信息都是由前端工程师写死的,而非从电商平台的数据库中读取后进行展示。因此,在实际操作中,活动页面上的信息,特别是价格数据,有可能和电商平台中的商品不一致,这个是我们在测试的时候需要重点关注的。

测试方式很简单,依次点击各个商品的链接,验证跳转的商品页面是否与促销活动页面中的商品信息一致即可。

2、页面跳转行为一致性

点击链接跳转页面,应该算是活动页面中唯一具有动作行为的功能了。而对于链接跳转而言,会存在两种形式,一种是在当前页面中加载商品页面,另一种是在新窗口中加载商品页面。

本来两种形式区别并不大,采用哪种形式都可以,但是从追求完美的角度出发,我们还是需要保证活动页面中的所有链接的跳转行为都是相同的。

因此,在测试时,逐一点击所有的链接,验证所有链接跳转行为是否一致即可。

3、页面兼容性

作为促销活动页面,虽然商品信息是最核心的内容,但是为了能吸引尽量多的用户,页面的设计往往花了很多功夫,力求精美。然而,用户访问活动页面的设备和浏览器五花八门,有可能是采用PC浏览器,有可能是采用iPhone设备,也可能是采用各种品牌和型号的Android设备,精心设计的活动页面在某些设备或浏览器上很有可能就出现样式混乱的情况。因此,促销活动页面的浏览器兼容性也是z在测试时需要重点关注的。

差异在哪儿呢?差异就是不同的浏览器内核,不同的设备操作系统,不同的屏幕分辨率。

当然,我们也不可能在所有类型的设备和浏览器上都测一遍,但是主流的浏览器内核和移动设备还是要尽量覆盖的。

推荐的测试方式如下:

  • 针对不同浏览器内核的测试,在电脑上采用Chrome、IE、Firefox、Safari浏览器分别加载活动页面;
  • 针对不同移动设备类型的测试,在iPhone和Android设备上加载活动页面,iPhone和Android设备均只选一款即可;
  • 针对不同屏幕分辨率的测试,可以在PC浏览器中打开开发者工具,里面可以模拟不同分辨率的设备加载页面,前面提到的浏览器基本都支持这个功能。

4、文案准确性

对于促销活动而言,吸引眼球的文案肯定是必不可少的,这也是在测试时需要重点关注的。

对于文案方面的测试,可以重点从以下几个方面进行考核。

首先,由于文案通常是由需求方提供,而活动页面是由设计师或前端工程师制作,因此有可能在制作页面的过程中出现了偏差,这个需要测试时仔细核对。

另外,文案中出现错别字的情况也比较多,这个也需要格外注意,尽量杜绝这样的低级错误。

还有一种情况,活动促销页面是面向某个国家的用户,语言可能是非汉语也非英语,这个时候周围谁也看不懂文案里面到底写的是啥。这个时候,只能请需求方再三进行确认,文案描述正确是一方面,另一方面就是需要考虑到目标国家的地域文化,不要出现产生歧义和误解的情况。

总结

以上内容便是从促销活动页面进行展开,联想到的关于测试的一些内容。

可以看出,即使是再简单的东西,也是需要进行测试的,而且测试需要考虑的因素也非常多。另一方面,这也说明测试并不应该仅仅局限于技术层面,只有当我们站在业务和质量保障的角度,才会有更开阔的视野。

敏捷团队协作:Confluence简易教程

发表于 2016-06-07 | 更新于 2019-04-03 | 分类于 效率工具

0、Confluence简介

Confluence是一个企业级的Wiki软件,可用于在企业、部门、团队内部进行信息共享和协同编辑。

1、基础概念

Confluence的使用并不复杂,只需掌握如下几个基础概念。

空间(Space)

空间是Confluence系统中的一个区域,用于存储wiki页面,并可实现对空间中的所有文档进行统一的权限管理。

通常,我们可以针对每个项目单独创建一个空间,然后将与该项目相关的文档信息放置到该空间中,并只对项目成员开设访问/编辑权限。

除了项目空间,每个成员都有一个个人空间。平时成员可以将工作总结或笔记等文档放置到自己的空间中;对于对团队有帮助的文档,就可以将文档移动至团队项目空间中。

Dashboard

Dashboard是Confluence系统的主页,在Dashboard界面中包含了Confluence站点中的所有空间列表,以及最近更新内容的列表。

页面(Page)

在Confluence系统中,页面是存储和共享信息的主要方式。页面可以互相链接、连接、组织和访问,并以树状结构进行组织,放置于空间之中。

页面遵循所见即所得的编辑方式,操作上简单易用。更强大的地方在于,页面支持大量的内容展现形式,除了富文本文档外,还包括图表、视频、附件(可预览)、流程图、公式等等;如果还不够,还可以通过海量的第三方插件进行扩展。

在页面中可以通过@其它成员,通知相关成员查看文档。文档保存成功后,被@的成员就会收到邮件,并可根据邮件中的链接访问到该文档,然后进行评论或者协同编辑。

模板(template)

创建页面时除了采用空白文档,也可以选择模板。模板是在空白文档的基础上,根据特定需求添加了一些文档要素,可辅助用户更好更快地创建文档。

Confluence内置了大量的模板,可辅助用于项目工作的各个环节,包括产品需求、会议记录、决策记录、指导手册(How-to)、回顾记录、工作计划、任务报告等等。并且由于Confluence和JIRA是同一家公司的产品,在Confluence中可以和JIRA进行无缝衔接,实现对产品质量实现更好的展现。

如果对Confluence自带的模板不满意,还可以对模板进行调整,或者根据自己的需求创建其它类型的模板。

权限(Permission)

在安全性方面,Confluence具有完善和精细的权限控制,可以很好地控制用户在Wiki中创建、编辑内容和添加注释。

权限控制分3个维度,分别是团队(Group),个人(Individual Users),匿名用户(Anonymous)。

使用团队级的权限控制时,需要在Confluence服务器中对公司员工进行分组,好处在于配置比较方便,只需要对整个团队进行统一的权限配置。

但在实际项目中,经常会存在同一个项目包含多个跨团队成员的情况,这个时候就不适合采用团队权限配置方式,只能采用逐个添加成员的方式,并对各个成员分别配置权限。

另外一种情况,就是对于未登录的用户,以及项目成员以外的用户,可以开设部分权限,例如只读(View)。

2、常见操作

熟悉了Confluence的基础概念,基本上就可以摸索着对Confluence进行上手了。不过,为了减少摸索时间,在这里我再将Confluence中的常用操作进行说明。

创建空间(Space)

新建一个项目时,首先要做的就是创建一个空间,并进行初始化配置。

创建空间的方式很简单,可以从顶部菜单进行创建:【Spaces】->【Create Space】;也可以从Dashboard页面的Spaces页面中进行创建。

Confluence Dashboard

进入创建空间页面后,需要选择空间类型。这个需要根据空间的用途进行选择,对于团队协作的空间,推荐选择“Team Space”,如果实在不知道选择什么类型,选择“Blank Space”也是可以的。

Create space in Confluence

然后是填写空间的基本信息。所有类型的空间都有两个必填字段,Space name和Space key。Space key可以理解为空间的ID,不同空间的Space key不能重复,但Space name是可以重复的。

另外,对于“Team Space”类型的空间,多了一个“Team members”字段,用于添加空间的成员。成员的名称是其公司邮箱的前缀。

需要说明的是,空间创建完成后,Space key字段是不能修改的,其它字段以及团队成员都可以进行修改。

Create team space in Confluence

配置空间权限

创建空间后,根据项目需要,可以给空间设置权限。只有空间的管理员才能对空间权限进行配置。

操作方式如下:首先进入空间的页面,在空间左下角中,【Space tools】->【Permissions】,进入权限管理页面。

Permissions menu of Confluence

Confluence的权限控制比较完善,可以根据团队规范进行较为精细粒度的设置。

Permissions settings of Confluence

添加文档

在Confluence中文件以树状结构进行组织。

推荐的创建方式是,先进入父目录的页面,然后再点击【Create】进行创建。在创建文档页面中,可以看到新建文档的“Parent”,表示新文档创建后将位于“Parent”文件的下一个层级中。

Create page in Confluence

在新建文档时,需要选择文档模板。这个就根据文档的实际类型或用途进行选择即可,如果觉得都不合适,就选择“Blank page”。

编写文档

在编写文档时,页面遵循所见即所得的编辑方式,基本上跟在MS Word中的操作类似。

Confluence也集成了许多编辑工具,可以很方便地插入图表、链接、附件、代办列表等等。如果还不满足需求,可以点击【Insert】->【Other macros】,查找更多的扩展插件。

Edit page of Confluence

例如,Confluence默认是不支持Markdown编辑模式的,如果想采用Markdown来编写文档,就可以通过上述方式到插件市场寻找Markdown的插件。

不过根据实践发现,当前Confluence的Markdown插件支持的还不够好,使用体验上不尽如人意。比较推荐的做法,还是在单独的Markdown编辑器上采用markdown语法进行编辑,编辑完成后进行预览,然后将渲染后的文档内容复制粘贴到Confluence中。

移动文档

很多时候我们需要调整目录结构,这就涉及到需要将文档移动到别的目录层级下。

操作方式如下:先进入到待移动的文档页面中,点击页面右上角的【…】->【Move】;

Move page menu of Confluence

然后选择新的目录即可。

Move page of Confluence

从0到1搭建移动App功能自动化测试平台(3):编写iOS自动化测试脚本

发表于 2016-05-30 | 更新于 2019-04-03 | 分类于 Testing , 自动化测试

通过前面三篇文章,我们已经将iOS自动化功能测试的开发环境全部准备就绪,也学习了iOS UI控件交互操作的一般性方法,接下来,就可以开始编写自动化测试脚本了。

在本文中,我将在M项目中挑选一个功能点,对其编写自动化测试脚本,演示编写自动化测试用例的整个流程。

语言的选择:Python or Ruby?

之前介绍Appium的时候也提到,Appium采用Client-Server的架构设计,并采用标准的HTTP通信协议;Client端基本上可以采用任意主流编程语言编写测试用例,包括但不限于C#、Ruby、Objective-C、Java、node.js、Python、PHP。

因此,在开始编写自动化测试脚本之前,首先需要选定一门编程语言。

这个选择因人而异,并不涉及到太大的优劣之分,基本上在上述几门语言中选择自己最熟悉的就好。

但对我而言,选择却没有那么干脆,前段时间在Python和Ruby之间犹豫了很久,经过艰难的决定,最终选择了Ruby。为什么不考虑Java?不熟是一方面,另一方面是觉得采用编译型语言写测试用例总感觉太重,这活儿还是解释型语言来做更合适些。

其实,最开始本来是想选择Python的,因为Python在软件测试领域比Ruby应用得更广,至少在国内,不管是公司团队,还是测试人员群体,使用Python的会比使用Ruby的多很多。

那为什么还是选择了Ruby呢?

我主要是基于如下几点考虑的:

  • 从Appium的官方文档来看,Appium对Ruby的支持力度,或者说是偏爱程度,貌似会更大些;在Appium Client Libraries列表中将Ruby排在第一位就不说了,在Appium Tutorials中示例语言就只采用了Ruby和Java进行描述。
  • Appium_Console是采用Ruby编写的,在Console中执行的命令基本上可直接用在Ruby脚本中。
  • 后续打算引入BDD(行为驱动开发)的测试模式,而不管是cucumber还是RSpec,都是采用Ruby开发的。

当然,还有最最重要的一点,身处于珠江三角洲最大的Ruby阵营,周围Ruby大牛云集,公司的好多业务系统也都是采用Rails作为后台语言,完全没理由不选择Ruby啊。

第一个测试用例:系统登录

在测试领域中,系统登录这个功能点的地位,堪比软件开发中的Hello World,因此第一个测试用例就毫无悬念地选择系统登录了。

在编写自动化测试脚本之前,我们首先需要清楚用例执行的路径,路径中操作涉及到的控件,以及被操作控件的属性信息。

对于本次演示的APP来说,登录时需要先进入【My Account】页面,然后点击【Login】进入登录页面,接着在登录页面中输入账号密码后再点击【Login】按钮,完成登录操作。

Preview of DebugTalk Plus login

确定了操作路径以后,就可以在Appium Ruby Console中依次操作一遍,目的是确保代码能正确地对控件进行操作。

第一步要点击【My Account】按钮,因此先查看下Button控件属性。要是不确定目标控件的类型,可以直接执行page命令,然后在返回结果中根据控件名称进行查找。

1
2
3
4
5
6
[1] pry(main)> page :button
...(略)
UIAButton
name, label: My Account
id: My Account => My Account
nil

通过返回结果,可以看到【My Account】按钮的name、label属性就是“My Account”,因此可以通过button_exact('My Account')方式来定位控件,并进行点击操作。

1
2
[2] pry(main)> button_exact('My Account').click
nil

执行命令后,观察iOS模拟器中APP的响应情况,看是否成功进入“My Account”页面。

第二步也是类似的,操作代码如下:

1
2
[3] pry(main)> button_exact('Login').click
nil

进入到登录页面后,再次查看页面中的控件信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
[4] pry(main)> page
...(略)
UIATextField
value: Email Address
id: Email Address => Email Address
UIASecureTextField
value: Password (6-16 characters)
id: Password (6-16 characters) => Password (6-16 characters)
UIAButton
name, label: Login
id: Log In => Login
登录 => Login
...(略)

第三步需要填写账号密码,账号密码的控件属性分别是UIATextField和UIASecureTextField。由于这两个控件的类型在登录页面都是唯一的,因此可以采用控件的类型来进行定位,然后进行输入操作,代码如下:

1
2
3
4
[5] pry(main)> tag('UIATextField').type 'leo.lee@debugtalk.com'
""
[6] pry(main)> tag('UIASecureTextField').type '123456'
""

执行完输入命令后,在iOS模拟器中可以看到账号密码输入框都成功输入了内容。

最后第四步点击【Login】按钮,操作上和第二步完全一致。

1
2
[7] pry(main)> button_exact('Login').click
nil

执行完以上四个步骤后,在iOS模拟器中看到成功完成账号登录操作,这说明我们的执行命令没有问题,可以用于编写自动化测试代码。整合起来,测试脚本就是下面这样。

1
2
3
4
5
button_exact('My Account').click
button_exact('Login').click
tag('UIATextField').type 'leo.lee@debugtalk.com'
tag('UIASecureTextField').type '12345678'
button_exact('Login').click

将以上脚本保存为login.rb文件。

但当我们直接运行login.rb文件时,并不能运行成功。原因很简单,脚本中的button_exact、tag这些方法并没有定义,我们在文件中也没有引入相关库文件。

在上一篇文章中有介绍过,通过arc启动虚拟机时,会从appium.txt中读取虚拟机的配置信息。类似的,我们在脚本中执行自动化测试时,也会加载虚拟机,因此同样需要在脚本中指定虚拟机的配置信息,并初始化Appium Driver的实例。

初始化代码可以通过Appium Inspector生成,基本上为固定模式,我们暂时不用深究。

添加初始化部分的代码后,测试脚本如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
require 'rubygems'
require 'appium_lib'

capabilities = {
'appium-version' => '1.0',
'platformName' => 'iOS',
'platformVersion' => '9.3',
}
Appium::Driver.new(caps: capabilities).start_driver
Appium.promote_appium_methods Object

# testcase: login
button_exact('My Account').click
button_exact('Login').click
tag('UIATextField').type 'leo.lee@debugtalk.com'
tag('UIASecureTextField').type '123456'
button_exact('Login').click

driver_quit

优化测试脚本:加入等待机制

如上测试脚本编写好后,在Terminal中运行ruby login.rb,就可以执行脚本了。

运行命令后,会看到iOS虚拟机成功启动,接着App成功进行加载,然后自动按照前面设计的路径,执行系统登录流程。

但是,在实际操作过程中,发现有时候运行脚本时会出现找不到控件的异常,异常信息如下所示:

1
2
3
4
5
6
7
8
➜ ruby login.rb
/Library/Ruby/Gems/2.0.0/gems/appium_lib-8.0.2/lib/appium_lib/common/helper.rb:218:in `_no_such_element': An element could not be located on the page using the given search parameters. (Selenium::WebDriver::Error::NoSuchElementError)
from /Library/Ruby/Gems/2.0.0/gems/appium_lib-8.0.2/lib/appium_lib/ios/helper.rb:578:in `ele_by_json'
from /Library/Ruby/Gems/2.0.0/gems/appium_lib-8.0.2/lib/appium_lib/ios/helper.rb:367:in `ele_by_json_visible_exact'
from /Library/Ruby/Gems/2.0.0/gems/appium_lib-8.0.2/lib/appium_lib/ios/element/button.rb:41:in `button_exact'
from /Library/Ruby/Gems/2.0.0/gems/appium_lib-8.0.2/lib/appium_lib/driver.rb:226:in `rescue in block (4 levels) in promote_appium_methods'
from /Library/Ruby/Gems/2.0.0/gems/appium_lib-8.0.2/lib/appium_lib/driver.rb:217:in `block (4 levels) in promote_appium_methods'
from login.rb:28:in `<main>'

更奇怪的是,这个异常并不是稳定出现的,有时候能正常运行整个用例,但有时在某个步骤就会抛出找不到控件的异常。这是什么原因呢?为什么在Appium Ruby Console中单步操作时就不会出现这个问题,但是在执行脚本的时候就会偶尔出现异常呢?

原来,在我们之前的脚本中,两条命令之间并没有间隔时间,有可能前一条命令执行完后,模拟器中的应用还没有完成下一个页面的加载,下一条命令就又开始查找控件,然后由于找不到控件就抛出异常了。

这也是为什么我们在Appium Ruby Console中没有出现这样的问题。因为手工输入命令多少会有一些耗时,输入两条命令的间隔时间足够虚拟机中的APP完成下一页面的加载了。

那针对这种情况,我们要怎么修改测试脚本呢?难道要在每一行代码之间都添加休眠(sleep)函数么?

也不用这么麻烦,针对这类情况,ruby_lib实现了wait机制。将执行命令放入到wait{}中后,执行脚本时就会等待该命令执行完成后再去执行下一条命令。当然,等待也不是无休止的,如果等待30秒后还是没有执行完,仍然会抛出异常。

登录流程的测试脚本修改后如下所示(已省略初始化部分的代码):

1
2
3
4
5
wait { button_exact('My Account').click }
wait { button_exact('Login').click }
wait { tag('UIATextField').type 'leo.lee@debugtalk.com' }
wait { tag('UIASecureTextField').type '123456' }
wait { button_exact('Login').click }

对脚本添加wait机制后,之前出现的找不到控件的异常就不再出现了。

优化测试脚本:加入结果检测机制

然而,现在脚本仍然不够完善。

我们在Appium Ruby Console中手工执行命令后,都是由人工肉眼确认虚拟机中APP是否成功进入下一个页面,或者返回结果是否正确。

但是在执行自动化测试脚本时,我们不可能一直去盯着模拟器。因此,我们还需要在脚本中加入结果检测机制,通过脚本实现结果正确性的检测。

具体怎么做呢?

原理也很简单,只需要在下一个页面中,寻找一个在前一个页面中没有的控件。

例如,由A页面跳转至B页面,在B页面中会存在“Welcome”的文本控件,但是在A页面中是没有这个“Welcome”文本控件的;那么,我们就可以在脚本中的跳转页面语句之后,加入一条检测“Welcome”文本控件的语句;后续在执行测试脚本的时候,如果页面跳转失败,就会因为找不到控件而抛出异常,我们也能通过这个异常知道测试执行失败了。

当然,对下一页面中的控件进行检测时同样需要加入等待机制的。

登录流程的测试脚本修改后如下所示(已省略初始化部分的代码):

1
2
3
4
5
6
7
8
9
10
wait { button_exact('My Account').click }
wait { text_exact 'System Settings' }

wait { button_exact('Login').click }
wait { button_exact 'Forget password?' }

wait { tag('UIATextField').type 'leo.lee@debugtalk.com' }
wait { tag('UIASecureTextField').type '12345678' }
wait { button_exact('Login').click }
wait { text_exact 'My Message' }

至此,系统登录流程的自动化测试脚本我们就编写完成了。

To be continued …

在本文中,我们通过系统登录这一典型功能点,演示了编写自动化测试用例的整个流程。

在下一篇文章中,我们还会对自动化测试脚本的结构进行进一步优化,并实现测试代码工程化。

1…456…8
solomiss

solomiss

75 日志
11 分类
50 标签
GitHub E-Mail
Creative Commons
© 2019 solomiss