引子

介绍一下自己几个月以来学习Calabash的经验,从一个对Calabash完全不了解的新手开始,到用calabash写过一个iPad项目,一个Android平板项目,外加一个iPhone项目的自动化测试,双手已经沾上一些Calabash的味道。好比一个小兵,端起枪上过战场,打过两场小规模战斗,长了点见识之后仍然是一个小兵,从一个完全的新手,变成一个尝过一点Calabash味道的新手。废话少说,下面介绍一下自己学习Calabash的经验:

安装

最开始的时候,安装一个稳定的版本组合很重要。

具体的安装步骤,请参考官方文档。这里只是提醒一下,对于Calabash来说,不同的版本组合会碰到不同问题,至少我自己一开始碰到不少问题,经过测试的版本组合才能推荐给其他人,才能升级CI环境。

对于一个新手来讲,选择错误的版本组合,意味着你碰到奇怪错误的机会增加。而刚开始就不能顺利上手,而是去研究Calabash,OS,iOS,SDK, 模拟器等各种版本兼容性问题是一件非常恼人的事情。虽然排错是很好的学习方式,但一开始就碰到兼容性问题可能会降低你的学习热情,以为Calabash只是一个不成熟的玩具,其实不然。

入门

如果你刚接触Calabash, 要看的东西并不多,下面这两个地方仔细看一遍就足够了。
iOS:
https://github.com/calabash/calabash-ios/wiki/_pages
Android:
https://github.com/calabash/calabash-android
其中的两个章节:03.5 Calabash iOS Ruby API 和 05 Query syntax, 在开始写脚本的时候会不断回头参考。

上手

下面假设你的环境没问题了,接下来该找个iOS或Android的具体项目开始写测试,光看懂Github上那个简单的demo,理解大致的原理远远不够, 真正动手写Calabash脚本的时候,你会碰到很多问题。具体来说,刚一开始你最常碰到的问题是:

  1. 如何定位元素
  2. 为什么运行结果有时候不稳定

对于第一个问题,如果你的app比较简单,元素的id, name属性齐全,可能问题不大。如果能看懂并修改源代码,或者让开发人员帮你补上帮助识别元素的属性最好。如果这个方法行不通,很多时候识别一个元素有很多种写法,99%的可能你能找到替代的写法。这个时候是你熟悉Calabash console, 熟悉Query syntax的最好时机。启动Calabash console, 定位你想要的元素,尝试使用各种Query syntax定位元素。

相对于Web浏览器上的开发工具,Calabash识别元素的方法还比较“原始”,需要用start_test_server_in_background打开console, 然后利用query去搜索想要的元素,比在Web浏览器上定位一个元素麻烦很多。calabash-ios提供了一个flash方法,用来确定元素找得是否准确,但calabash-android没有提供对等的方法,所以不能用元素高亮来定位。

值得一提的是,在我测试的Android应用上面,通常id, name这样的属性不唯一,利用id或name去query, 往往会返回一组id相同的元素,其中大部分的x,y坐标不在可见范围内,这种情况需要利用坐标继续过滤返回结果。一个比较好的方法是自己写一个类去继承Calabash::ABase,然后自定义一个query(uiquery, *args)方法,过滤掉x,y坐标不在可见范围的元素。

对于第二个问题,用Calabash写UI自动化集成测试和回归测试,如果运行结果不稳定的话价值就不大了。这个时候是练习使用trait, await, wait_for, element_exists等方法,熟悉Calabash API的最好时机。
写Calabash测试,或者写其他任何UI测试的时候,直觉上想消灭所有sleep语句。但完全消灭sleep并不简单,尤其对于calabash-android来说, 目前没有wait_for_none_animating方法, 需要自己寻找等待策略,但这个时候也是积累经验的最好时机,多尝试一些wait_for 和 element_exists方法,让你的测试脚本变得更稳定。

书写易于维护的测试

如果进行到此一切顺利,你已经对元素定位,提高脚本的稳定性有了一些经验,接下来你可以写很多代码开始做项目了。

当你的代码量不断增加,你会开始考虑如何组织你的测试代码,如何写易于维护的代码这个大问题。Page Object Model是一个不错的选择,也是Calabash官方推荐的方式。关于Page Object Model,网上已经有很多讨论,以后有时间的时候可以另开一贴讨论。

除了POM以外,有些其他话题可以先讨论一下:

避免嵌套使用macro语法
完全不使用macro难免会写很多重复的steps, 但macro嵌套使用会大大增加维护成本,代码也将变得不易读。建议:可以使用macro, 但一定避免嵌套使用。

避免过于琐碎的cucumber story
比如一个Successful login scenario可以像下面这样写:

Given I am on login screen
And I input my username
And I input my password
And I click on "Login" button
Then I should login successfully. 

单独写这样一个login scenario没关系,但是几乎每个scenario都需要login, 因此需要再写一个简短版本的login语句。
上面只是一个简单的例子, Cucumber可以有两种书写风格 imperativedeclarative,前一种书写过多细节,后一种隐藏细节,两种风格任意一种走向极致都不好,但书写过于具体的步骤肯定不推荐,可以阅读《The Cucumber Book》中的第六章 “When Cucumbers Go Bad” 避免写出琐碎且不易为维护的story。

跨平台测试的代码如何组织
通常一个移动app会有iOS和Android两个版本,功能界面和用户体验在两个平台上大致相同,因此测试的features和scenarios会有大部分重叠。不仅如此,写测试脚本通常要为两个平台写很多辅助代码, 比如调用backend service, 访问数据库,以及一些自定义辅助方法很可能在两个平台共享。从代码重用的角度,有理由把iOS和Android的测试代码放在一起,比如官方提供个一个组织跨平台代码的例子:https://github.com/calabash/x-platform-example

然而,把Android和iOS的测试代码放在一起也有一些很明显的缺点:

  1. 不方便搜索,即使是自己写的代码,当file和folder的数量不断增加,调试代码的时候经常需用editor进行全目录搜索。将iOS和Android代码放在同一目录下,使每次搜索返回两个结果:一个在ios目录里面,另一个在android目录里面,不小心容易点错,而去编辑另一个平台的代码。
  2. 对于两个平台共用部分的代码,每做一次修改,需要考虑两个平台上的结果,需要在两个平台分别验证结果。
  3. 不方便运行单个测试从而不方便调试。尽管Cucumber提供了profile机制,可以为iOS或Android加载不同的目录,但每运行一个测试都需要指定profile, 是不必要的负担。
  4. 两个平台的产品也会有差异,有些test case不会完全相同,在同一个feature里面可能会分别打上 @ios, @android, 或@common这样的tags, 当scenarios的数量开始增加,利用tag机制区分同一个目录,甚至是同一个文件下两个平台的代码,不是一个好方法,间隔时间稍微久一点就容易混淆,因此这样的tagging方式不建议使用。

总之,要不要放在一起要看测试代码的规模,两个平台上实现的异同,共用代码的数量,多少人共同开发维护脚本来决定。个人建议:对于比较简单的项目,可以把两个平台的测试代码放在一起,否则还是分开更好。

结尾

最后,用一个采用PMO style写Calabash测试可能遇到的一个小错误作为结尾:
下面这段代码来自网上(http://rubygemtsl.com/tag/calabash-android/

class WordPressApp < DroidPress

def welcome_screen
@welcome_screen ||= page(WelcomeScreen)
end

def login_screen
@login_screen ||= page(LoginScreen)
end

def home_screen
@home_screen ||= page(HomeScreen)
end

end

welcome_screen方法第一次被调用的时候从page方法返回一个object, 之后的调用直接返回@welcome_screen,经常写ruby代码的同学会习惯这样写,但是上面这段代码当welcome_screen更新后结果就不一定准确了,因此要写成下面这种“低效”的方式,每次使用welcome_screen都要从page()那里再次获取

class WordPressApp < DroidPress

def welcome_screen
page(WelcomeScreen).await
end

end

实际上,对于通过UI进行测试的代码,慢的地方一般不在ruby写法是否高效,即使每次都从page()方法获取object和直接返回@welcome_screen完全感觉不到区别。

没想到会写这么多, 抛砖引玉,欢迎各位指教。


↙↙↙阅读原文可查看相关链接,并与作者交流